@@ -218,6 +218,78 @@ const WellSelectorComponent = () => {
218218 dispatch ( wellSelectorSlice . setCupSelectOverlap ( value ) ) ;
219219 } ;
220220
221+ //##################################################################################
222+ // Save positions to JSON file
223+ const handleSavePositions = ( ) => {
224+ try {
225+ const positionsData = {
226+ pointList : experimentState . pointList ,
227+ wellLayout : experimentState . wellLayout ,
228+ savedAt : new Date ( ) . toISOString ( ) ,
229+ } ;
230+
231+ const dataStr = JSON . stringify ( positionsData , null , 2 ) ;
232+ const dataBlob = new Blob ( [ dataStr ] , { type : "application/json" } ) ;
233+ const url = URL . createObjectURL ( dataBlob ) ;
234+
235+ const link = document . createElement ( "a" ) ;
236+ link . href = url ;
237+ link . download = `wellplate_positions_${ new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) } .json` ;
238+ link . click ( ) ;
239+
240+ URL . revokeObjectURL ( url ) ;
241+
242+ infoPopupRef . current ?. showInfo ( "Positions saved successfully!" ) ;
243+ } catch ( error ) {
244+ console . error ( "Error saving positions:" , error ) ;
245+ infoPopupRef . current ?. showInfo ( "Error saving positions!" ) ;
246+ }
247+ } ;
248+
249+ //##################################################################################
250+ // Load positions from JSON file
251+ const handleLoadPositions = ( ) => {
252+ const input = document . createElement ( "input" ) ;
253+ input . type = "file" ;
254+ input . accept = "application/json" ;
255+
256+ input . onchange = ( e ) => {
257+ const file = e . target . files [ 0 ] ;
258+ if ( ! file ) return ;
259+
260+ const reader = new FileReader ( ) ;
261+ reader . onload = ( event ) => {
262+ try {
263+ const positionsData = JSON . parse ( event . target . result ) ;
264+
265+ // Validate data structure
266+ if ( ! positionsData . pointList || ! Array . isArray ( positionsData . pointList ) ) {
267+ throw new Error ( "Invalid positions file format" ) ;
268+ }
269+
270+ // Load positions into Redux state
271+ dispatch ( experimentSlice . setPointList ( positionsData . pointList ) ) ;
272+
273+ // Optionally restore layout if included
274+ if ( positionsData . wellLayout ) {
275+ dispatch ( experimentSlice . setWellLayout ( positionsData . wellLayout ) ) ;
276+ }
277+
278+ infoPopupRef . current ?. showInfo (
279+ `Loaded ${ positionsData . pointList . length } position(s) successfully!`
280+ ) ;
281+ } catch ( error ) {
282+ console . error ( "Error loading positions:" , error ) ;
283+ infoPopupRef . current ?. showInfo ( "Error loading positions file!" ) ;
284+ }
285+ } ;
286+
287+ reader . readAsText ( file ) ;
288+ } ;
289+
290+ input . click ( ) ;
291+ } ;
292+
221293 //##################################################################################
222294 return (
223295 < div style = { { border : "0px solid #eee" , padding : "10px" } } >
@@ -263,115 +335,63 @@ const WellSelectorComponent = () => {
263335 </ Select >
264336 </ FormControl >
265337
266- { /* TILE OVERLAP SLIDER */ }
267- < Box sx = { { width : 250 , marginLeft : "20px" , marginRight : "20px" } } >
268- < Typography variant = "body2" gutterBottom >
269- Tile Overlap: { Math . round ( ( experimentState . parameterValue . overlapWidth || 0 ) * 100 ) } %
270- </ Typography >
271- < Slider
272- value = { ( experimentState . parameterValue . overlapWidth || 0 ) * 100 }
273- onChange = { ( e , newValue ) => {
274- const overlapValue = newValue / 100 ;
275- // Update both states to keep them in sync
276- dispatch ( wellSelectorSlice . setOverlapWidth ( overlapValue ) ) ;
277- dispatch ( wellSelectorSlice . setOverlapHeight ( overlapValue ) ) ;
278- dispatch ( experimentSlice . setOverlapWidth ( overlapValue ) ) ;
279- dispatch ( experimentSlice . setOverlapHeight ( overlapValue ) ) ;
280- } }
281- min = { 0 }
282- max = { 50 }
283- step = { 5 }
284- marks = { [
285- { value : 0 , label : '0%' } ,
286- { value : 25 , label : '25%' } ,
287- { value : 50 , label : '50%' } ,
288- ] }
289- valueLabelDisplay = "auto"
290- valueLabelFormat = { ( value ) => `${ value } %` }
291- />
338+ { /* VIEW - All controls in one row */ }
339+ < Box sx = { { display : "flex" , gap : 1 , alignItems : "center" , flexWrap : "wrap" } } >
340+ < Button
341+ variant = "contained"
342+ size = "small"
343+ onClick = { ( ) => handleResetView ( ) }
344+ >
345+ Reset View
346+ </ Button >
347+
348+ < Button
349+ variant = "contained"
350+ size = "small"
351+ onClick = { ( ) => handleResetHistory ( ) }
352+ >
353+ Reset History
354+ </ Button >
355+
356+ < label style = { { fontSize : "14px" , display : "flex" , alignItems : "center" , gap : "4px" } } >
357+ < input
358+ type = "checkbox"
359+ checked = { wellSelectorState . showOverlap }
360+ onChange = { handleShowOverlapChange }
361+ />
362+ Show Overlap
363+ </ label >
364+
365+ < label style = { { fontSize : "14px" , display : "flex" , alignItems : "center" , gap : "4px" } } >
366+ < input
367+ type = "checkbox"
368+ checked = { wellSelectorState . showShape }
369+ onChange = { handleShowShapeChange }
370+ />
371+ Show Shape
372+ </ label >
292373 </ Box >
293374
294- { /* VIEW */ }
295-
296- < Button variant = "contained" onClick = { ( ) => handleResetView ( ) } >
297- reset view
298- </ Button >
299-
300- < Button
301- variant = "contained"
302- onClick = { ( ) => handleResetHistory ( ) }
303- style = { { marginLeft : "10px" } }
304- >
305- reset history
306- </ Button >
307-
308- < label style = { { fontSize : "14px" } } >
309- < input
310- type = "checkbox"
311- checked = { wellSelectorState . showOverlap }
312- onChange = { handleShowOverlapChange }
313- />
314- show overlap
315- </ label >
316-
317- < label style = { { fontSize : "14px" } } >
318- < input
319- type = "checkbox"
320- checked = { wellSelectorState . showShape }
321- onChange = { handleShowShapeChange }
322- />
323- show shape
324- </ label >
325- </ div >
375+ { /* Save/Load Positions */ }
376+ < Box sx = { { display : "flex" , gap : 1 , mt : 1 } } >
377+ < Button
378+ variant = "outlined"
379+ size = "small"
380+ onClick = { handleSavePositions }
381+ disabled = { ! experimentState . pointList || experimentState . pointList . length === 0 }
382+ >
383+ 💾 Save Positions
384+ </ Button >
326385
327- { /* ADVANCED SETTINGS */ }
328- < Accordion style = { { marginBottom : "10px" } } >
329- < AccordionSummary
330- expandIcon = { < ExpandMoreIcon /> }
331- aria-controls = "advanced-settings-content"
332- id = "advanced-settings-header"
333- >
334- < Typography > Advanced Layout Settings</ Typography >
335- </ AccordionSummary >
336- < AccordionDetails >
337- < Box display = "flex" flexDirection = "column" gap = { 2 } >
338- < Typography variant = "body2" color = "textSecondary" >
339- Global layout offsets (applied to all wells in micrometers):
340- </ Typography >
341-
342- < FormControl fullWidth >
343- < TextField
344- label = "Layout Offset X (μm)"
345- type = "number"
346- value = { wellSelectorState . layoutOffsetX || 0 }
347- onChange = { handleLayoutOffsetXChange }
348- inputProps = { {
349- step : 100 ,
350- } }
351- helperText = "Horizontal offset for all wells (default: 0)"
352- />
353- </ FormControl >
354-
355- < FormControl fullWidth >
356- < TextField
357- label = "Layout Offset Y (μm)"
358- type = "number"
359- value = { wellSelectorState . layoutOffsetY || 0 }
360- onChange = { handleLayoutOffsetYChange }
361- inputProps = { {
362- step : 100 ,
363- } }
364- helperText = "Vertical offset for all wells (default: 0)"
365- />
366- </ FormControl >
367-
368- < Typography variant = "caption" color = "textSecondary" style = { { marginTop : '8px' } } >
369- Note: Changes are applied immediately to the current layout.
370- For Wellplate 384, default startX=29490.625μm, startY=30688.125μm
371- </ Typography >
372- </ Box >
373- </ AccordionDetails >
374- </ Accordion >
386+ < Button
387+ variant = "outlined"
388+ size = "small"
389+ onClick = { handleLoadPositions }
390+ >
391+ 📂 Load Positions
392+ </ Button >
393+ </ Box >
394+ </ div >
375395
376396 { /* MODE */ }
377397 < div
@@ -437,69 +457,6 @@ const WellSelectorComponent = () => {
437457 </ ButtonGroup >
438458 </ div >
439459
440- { /* MODE-SPECIFIC SETTINGS */ }
441- { /* Area Select Settings */ }
442- { wellSelectorState . mode === Mode . AREA_SELECT && (
443- < Box
444- sx = { {
445- marginBottom : "15px" ,
446- padding : "12px" ,
447- border : ( theme ) => `1px solid ${ theme . palette . divider } ` ,
448- borderRadius : "4px" ,
449- backgroundColor : ( theme ) => theme . palette . mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : '#f9f9f9' ,
450- } }
451- >
452- < Typography variant = "subtitle2" gutterBottom >
453- Area Select Settings
454- </ Typography >
455- < Box display = "flex" flexDirection = "column" gap = { 2 } >
456- { /* Snakescan checkbox */ }
457- < FormControlLabel
458- control = {
459- < input
460- type = "checkbox"
461- checked = { wellSelectorState . areaSelectSnakescan }
462- onChange = { handleAreaSelectSnakescanChange }
463- />
464- }
465- label = "Enable Snakescan Pattern"
466- />
467- </ Box >
468- </ Box >
469- ) }
470-
471- { /* Cup/Well Select Settings */ }
472- { wellSelectorState . mode === Mode . CUP_SELECT && (
473- < Box
474- sx = { {
475- marginBottom : "15px" ,
476- padding : "12px" ,
477- border : ( theme ) => `1px solid ${ theme . palette . divider } ` ,
478- borderRadius : "4px" ,
479- backgroundColor : ( theme ) => theme . palette . mode === 'dark' ? 'rgba(255, 255, 255, 0.05)' : '#f9f9f9' ,
480- } }
481- >
482- < Typography variant = "subtitle2" gutterBottom >
483- Well Select Settings
484- </ Typography >
485- < Box display = "flex" flexDirection = "column" gap = { 2 } >
486- { /* Shape selector */ }
487- < FormControl size = "small" sx = { { width : "200px" } } >
488- < InputLabel > Well Shape</ InputLabel >
489- < Select
490- value = { wellSelectorState . cupSelectShape }
491- onChange = { handleCupSelectShapeChange }
492- label = "Well Shape"
493- >
494- < MenuItem value = "circle" > Circle</ MenuItem >
495- < MenuItem value = "rectangle" > Rectangle</ MenuItem >
496- </ Select >
497- < FormHelperText > Shape pattern for well scanning</ FormHelperText >
498- </ FormControl >
499- </ Box >
500- </ Box >
501- ) }
502-
503460 < InfoPopup ref = { infoPopupRef } />
504461 </ div >
505462 ) ;
0 commit comments