@@ -30,9 +30,11 @@ import {
3030import { EMPTY , getSlotInLocationStack } from '@opentrons/step-generation'
3131
3232import { LabwareOnDeck } from '/protocol-designer/components/organisms'
33+ import { SelectionRect } from '/protocol-designer/components/organisms/Labware/SelectionRect'
3334import { getRobotType } from '/protocol-designer/file-data/selectors'
3435import { getRobotStateAtActiveItem } from '/protocol-designer/top-selectors/labware-locations'
3536import { getLabwareNicknamesById } from '/protocol-designer/ui/labware/selectors'
37+ import { getCollidingWells } from '/protocol-designer/utils/index'
3638
3739import { INACCESSIBLE_PARTIAL_TIP } from '../NozzleAndWellSelectionModal/constants'
3840import { getEntireWellSelection } from '../NozzleAndWellSelectionModal/utils'
@@ -61,14 +63,13 @@ import type {
6163 PipetteV2Specs ,
6264 PrimaryNozzleConfigurationStyle ,
6365} from '@opentrons/shared-data'
66+ import type { GenericRect } from '/protocol-designer/collision-types'
6467import type {
6568 AccessibilityStatus ,
6669 InaccessibleReason ,
6770 TipSelectionBaseProps ,
6871} from './types'
6972
70- const NINETY_SIX_ALL_TARGET_WELL = 'A1'
71-
7273export function SelectTips (
7374 props : TipSelectionBaseProps & {
7475 pipetteSpecs : PipetteV2Specs
@@ -84,9 +85,9 @@ export function SelectTips(
8485 const { t } = useTranslation ( 'tip_selection' )
8586 const labwareNicknamesById = useSelector ( getLabwareNicknamesById )
8687 const robotType = useSelector ( getRobotType )
87- const [ hoveredWell , setHoveredWell ] = useState < string | null > ( null )
88+ const [ hoveredWells , setHoveredWells ] = useState < string [ ] | null > ( null )
8889 const leaveTimeoutRef = useRef < NodeJS . Timeout | null > ( null )
89- const currentHoveredWellRef = useRef < string | null > ( null )
90+ const currentHoveredWellRef = useRef < string [ ] | null > ( null )
9091
9192 const {
9293 pipetteSpecs,
@@ -126,13 +127,7 @@ export function SelectTips(
126127 ? ( tipAccessibilityStatus [ selectedTiprackId ] ?? { } )
127128 : { }
128129
129- const allWellsAffectedByHover = getEntireWellSelection (
130- hoveredWell ,
131- labwareDef . ordering ,
132- nozzles ,
133- primaryNozzle ,
134- channels
135- )
130+ const allWellsAffectedByHover = hoveredWells ?? [ ]
136131
137132 const areAllHoveredWellsAccessibleAndOccupied = allWellsAffectedByHover . every (
138133 well =>
@@ -166,6 +161,22 @@ export function SelectTips(
166161 const numPickupsRemaining = numTotalPickups - selectedTips . length
167162 const hasPickupsRemaining = numPickupsRemaining > 0
168163
164+ const _getWellsFromRect : ( rect : GenericRect ) => string [ ] [ ] = rect => {
165+ const wellsInRect = getCollidingWells ( rect )
166+ const highlightedWells : string [ ] [ ] = [ ]
167+ for ( const well in wellsInRect ) {
168+ const wellSelection = getEntireWellSelection (
169+ well ,
170+ labwareDef . ordering ,
171+ nozzles ,
172+ primaryNozzle ,
173+ channels
174+ )
175+ highlightedWells . push ( wellSelection )
176+ }
177+ return highlightedWells
178+ }
179+
169180 const handleUnselectWell = ( unselectIndex : number ) : void => {
170181 setSelectedTips ( selectedTips . slice ( 0 , unselectIndex ) )
171182 }
@@ -180,7 +191,6 @@ export function SelectTips(
180191 } ,
181192 { }
182193 )
183-
184194 if (
185195 // always allow tip unselection
186196 ! ( wellName in prevSelectedTipsByIndex ) &&
@@ -255,40 +265,91 @@ export function SelectTips(
255265
256266 const handleHoverWell = ( e : WellMouseEvent ) : void => {
257267 const { wellName } = e
258- let transformedWellName = wellName
259- if (
260- ( channels === 8 && nozzles === ALL ) ||
261- ( channels === 96 && nozzles === COLUMN )
262- ) {
263- const column = wellName . slice ( 1 , wellName . length )
264- transformedWellName = `A${ column } `
265- } else if ( channels === 96 && nozzles === ALL ) {
266- transformedWellName = NINETY_SIX_ALL_TARGET_WELL
267- } else if ( channels === 96 && nozzles === ROW ) {
268- const rowName = wellName . slice ( 0 , 1 )
269- transformedWellName = `${ rowName } 1`
270- }
268+ const allHoveredWells = getEntireWellSelection (
269+ wellName ,
270+ labwareDef . ordering ,
271+ nozzles ,
272+ primaryNozzle ,
273+ channels
274+ )
275+
271276 if ( leaveTimeoutRef . current ) {
272277 clearTimeout ( leaveTimeoutRef . current )
273278 leaveTimeoutRef . current = null
274279 }
275- setHoveredWell ( transformedWellName )
276- currentHoveredWellRef . current = transformedWellName
280+ setHoveredWells ( allHoveredWells )
281+ currentHoveredWellRef . current = allHoveredWells
277282 }
278283
279284 const handleLeaveWell = ( _ : WellMouseEvent ) : void => {
280285 if ( leaveTimeoutRef . current ) {
281286 clearTimeout ( leaveTimeoutRef . current )
282287 }
283288 leaveTimeoutRef . current = setTimeout ( ( ) => {
284- if ( currentHoveredWellRef . current === hoveredWell ) {
285- setHoveredWell ( null )
286- currentHoveredWellRef . current = null
287- }
289+ setHoveredWells ( null )
290+ currentHoveredWellRef . current = null
291+
288292 leaveTimeoutRef . current = null
289293 } , 300 )
290294 }
295+ const selectedWellsByIndex = selectedTips . reduce < Record < string , number > > (
296+ ( acc , tipList , index ) => {
297+ const innerAcc = tipList . reduce < Record < string , number > > (
298+ ( acc , tip ) => ( { ...acc , [ tip ] : index } ) ,
299+ { }
300+ )
301+ return { ...acc , ...innerAcc }
302+ } ,
303+ { }
304+ )
305+ const handleSelectionMove = ( e : MouseEvent , rect : GenericRect ) : void => {
306+ if ( ! e . shiftKey ) {
307+ const wellsUnderRect = _getWellsFromRect ( rect )
308+ const flat = [ ...new Set ( wellsUnderRect . flat ( ) ) ]
309+ setHoveredWells ( flat )
310+ }
311+ }
312+ const handleSelectionDone = ( e : MouseEvent , rect : GenericRect ) : void => {
313+ if ( ! e . shiftKey ) {
314+ const wellsUnderRect = _getWellsFromRect ( rect )
315+ const filteredWellsUnderRect = wellsUnderRect . filter ( wellGroup =>
316+ wellGroup . every (
317+ wellName =>
318+ tipAccessibileStatusByWellName [ wellName ] . isAccessible &&
319+ hasPickupsRemaining
320+ )
321+ )
322+ setSelectedTips ( prev => {
323+ const next = [ ...prev ]
324+ const selectedFlat = new Set ( prev . flat ( ) )
325+ const allAlreadySelected = filteredWellsUnderRect . every ( group =>
326+ group . every ( well => selectedFlat . has ( well ) )
327+ )
291328
329+ let updated : string [ ] [ ]
330+ // Remove all wells if the entire selection is already selected
331+ if ( allAlreadySelected ) {
332+ const keysToRemove = new Set ( wellsUnderRect . map ( g => g [ 0 ] ) )
333+ updated = next . filter ( group => ! keysToRemove . has ( group [ 0 ] ) )
334+ } else {
335+ // Add additional selected wells if there is a mixture of selected and unselected
336+ updated = [ ...next ]
337+ wellsUnderRect . forEach ( wellGroup => {
338+ const primaryWellInGroup = wellGroup [ 0 ]
339+ const exists = updated . some (
340+ wellGroup => wellGroup [ 0 ] === primaryWellInGroup
341+ )
342+ // Add to update list if the well does not currently exist in the list and is accessible
343+ if ( ! exists && selectedWellsByIndex [ primaryWellInGroup ] !== 1 ) {
344+ updated . push ( wellGroup )
345+ }
346+ } )
347+ }
348+ return updated
349+ } )
350+ setHoveredWells ( null )
351+ }
352+ }
292353 let controls : JSX . Element = < > </ >
293354 const labware = activeDeckSetup . labware [ selectedTiprackId ?? '' ]
294355 const slot = getSlotInLocationStack ( labware . stack )
@@ -299,16 +360,7 @@ export function SelectTips(
299360 controls = < > </ >
300361 } else {
301362 const tipState = robotState ?. tipState . tipracks [ selectedTiprackId ?? '' ]
302- const selectedWellsByIndex = selectedTips . reduce < Record < string , number > > (
303- ( acc , tipList , index ) => {
304- const innerAcc = tipList . reduce < Record < string , number > > (
305- ( acc , tip ) => ( { ...acc , [ tip ] : index } ) ,
306- { }
307- )
308- return { ...acc , ...innerAcc }
309- } ,
310- { }
311- )
363+
312364 const is96Channel = channels === 96
313365 const tipStatusByWellName =
314366 tipState != null
@@ -363,20 +415,20 @@ export function SelectTips(
363415 borderStroke = { COLORS . yellow40 }
364416 ignoreMissingTips
365417 />
366- { hoveredWell != null ? (
418+ { hoveredWells != null ? (
367419 < PipetteShadow
368420 robotType = { robotType }
369421 pipetteSpec = { pipetteSpecs }
370422 slotPosition = { slotPosition }
371- hoveredWell = { hoveredWell }
423+ hoveredWell = { hoveredWells [ 0 ] }
372424 selectedLabwareId = { selectedTiprackId }
373425 labwareState = { activeDeckSetup . labware }
374426 isHoveredWellSelected = { selectedTips
375427 . flat ( )
376- . some ( tip => tip === hoveredWell ) }
428+ . some ( tip => tip === hoveredWells [ 0 ] ) }
377429 hasPickupsRemaining = { hasPickupsRemaining }
378430 isAccessible = { hoveredWellsInaccessibilityStatus == null }
379- inaccessibleReason = { hoveredWellsInaccessibilityStatus }
431+ inaccessibleReason = { hoveredWellsInaccessibilityStatus ?? null }
380432 primaryNozzle = { primaryNozzle }
381433 enclosingViewbox = { viewBox }
382434 nozzles = { nozzles }
@@ -404,12 +456,18 @@ export function SelectTips(
404456 </ Flex >
405457 < div className = { styles . modal_body_select_tips } >
406458 < div className = { styles . select_tips_deck_container } >
407- < BaseDeckTipSelection
408- viewBox = { viewBox }
409- showSlotLabels = { false }
410- controls = { controls }
411- labwareIdToHide = { selectedTiprackId }
412- />
459+ < SelectionRect
460+ onSelectionMove = { handleSelectionMove }
461+ onSelectionDone = { handleSelectionDone }
462+ customWidth = { 45 }
463+ >
464+ < BaseDeckTipSelection
465+ viewBox = { viewBox }
466+ showSlotLabels = { false }
467+ controls = { controls }
468+ labwareIdToHide = { selectedTiprackId }
469+ />
470+ </ SelectionRect >
413471 </ div >
414472 < div className = { styles . legend_box } >
415473 < SelectionLegend selectionType = { TIP } size = { DEFAULT_TIP_SIZE } />
0 commit comments