88 getModuleDef ,
99 getOt2SurroundingSlots ,
1010 getPositionFromSlotId ,
11+ getRobotDefFromRobotType ,
1112 OT2_ROBOT_TYPE ,
1213 SINGLE ,
1314 THERMOCYCLER_MODULE_TYPE ,
@@ -36,8 +37,6 @@ import type {
3637 TipState ,
3738} from '../types'
3839
39- const A12_column_front_left_bound = { x : - 11.03 , y : 2 }
40- const A12_column_back_right_bound = { x : 526.77 , y : 506.2 }
4140export const PRIMARY_NOZZLE = 'A12'
4241const FLEX_TC_LID_COLLISION_ZONE = {
4342 back_left : { x : - 43.25 , y : 454.9 , z : 211.91 } ,
@@ -65,26 +64,6 @@ export interface Point {
6564 z ?: number
6665}
6766
68- // check if nozzle(s) are inbounds
69- const getIsWithinPipetteExtents = (
70- location : Point ,
71- nozzleConfiguration : NozzleConfigurationStyle ,
72- primaryNozzle : string
73- ) : boolean => {
74- if ( nozzleConfiguration === 'COLUMN' && primaryNozzle === 'A12' ) {
75- const isWithinBounds =
76- A12_column_front_left_bound . x <= location . x &&
77- location . x <= A12_column_back_right_bound . x &&
78- A12_column_front_left_bound . y <= location . y &&
79- location . y <= A12_column_back_right_bound . y
80-
81- return isWithinBounds
82- } else {
83- // TODO: Handle other configurations such as 8-channel partial tip, and eventually all pipettes.
84- return true
85- }
86- }
87-
8867// return pipette bounds at a sepcific position
8968// note that this calculation is pessimistic to mirror behavior on protocol engine
9069// the returned plane is defined by the z-height of the empty nozzles (lowest case scenario)
@@ -287,7 +266,7 @@ const getWellPosition = (
287266 labwareEntity : LabwareEntity ,
288267 wellName : string ,
289268 wellLocationOffset : Point ,
290- addressableAreaOffset : CoordinateTuple | null ,
269+ addressableAreaOffset : CoordinateTuple ,
291270 hasTip : boolean
292271) : Point => {
293272 const { wells } = labwareEntity . def
@@ -368,12 +347,27 @@ export const getIsSafePipetteMovement = (args: {
368347 const addressableAreaOffset = getPositionFromSlotId (
369348 labwareSlot ,
370349 deckDefinition
371- )
350+ ) ?? [ 0 , 0 , 0 ]
351+ const isOnFlexThermocycler =
352+ robotType === FLEX_ROBOT_TYPE &&
353+ labwareState [ labwareId ] . stack . some (
354+ item => moduleEntities [ item ] ?. type === THERMOCYCLER_MODULE_TYPE
355+ )
356+ const thermocyclerOffset = isOnFlexThermocycler
357+ ? ( deckDefinition . locations . addressableAreas . find (
358+ addressableArea => addressableArea . id === 'thermocyclerModuleV2'
359+ ) ?. offsetFromCutoutFixture ?? [ 0 , 0 , 0 ] )
360+ : [ 0 , 0 , 0 ]
361+ const fullOffset = [
362+ thermocyclerOffset [ 0 ] + addressableAreaOffset [ 0 ] ,
363+ thermocyclerOffset [ 1 ] + addressableAreaOffset [ 1 ] ,
364+ thermocyclerOffset [ 2 ] + addressableAreaOffset [ 2 ] ,
365+ ]
372366 const wellTargetPoint = getWellPosition (
373367 labwareEntities [ labwareId ] ,
374368 wellTargetName ,
375369 wellLocationOffset ,
376- addressableAreaOffset ,
370+ fullOffset as CoordinateTuple ,
377371 pipetteHasTip
378372 )
379373
@@ -385,14 +379,6 @@ export const getIsSafePipetteMovement = (args: {
385379 channels,
386380 } )
387381
388- const isWithinPipetteExtents = getIsWithinPipetteExtents (
389- wellTargetPoint ,
390- nozzleConfiguration ,
391- primaryNozzle
392- )
393- if ( ! isWithinPipetteExtents ) {
394- return false
395- }
396382 const tiprackEntity = tiprackId != null ? labwareEntities [ tiprackId ] : null
397383 const tipOverlapOnNozzle =
398384 tiprackEntity != null
@@ -409,6 +395,14 @@ export const getIsSafePipetteMovement = (args: {
409395 primaryNozzle ,
410396 tipOverlapOnNozzle
411397 )
398+ const isWithinPipetteExtents = getIsMovementWithinDeckExtents ( {
399+ channels,
400+ boundingBox : pipetteBoundsAtWellLocation ,
401+ robotType,
402+ } )
403+ if ( ! isWithinPipetteExtents ) {
404+ return false
405+ }
412406 const surroundingSlots =
413407 robotType === OT2_ROBOT_TYPE
414408 ? getOt2SurroundingSlots ( labwareSlot as OT2AddressableAreaName )
@@ -652,3 +646,44 @@ const getOverlapKeyForPipetteSpecs = (
652646 // default
653647 return 'Full'
654648}
649+
650+ const getIsMovementWithinDeckExtents = ( args : {
651+ channels : PipetteChannels
652+ boundingBox : Point [ ]
653+ robotType : RobotType
654+ } ) : boolean => {
655+ const { channels, boundingBox, robotType } = args
656+ const robotDef = getRobotDefFromRobotType ( robotType )
657+ const { paddingOffsets } = robotDef
658+ const { front, rear, leftSide, rightSide } = paddingOffsets
659+ const [ xExtent , yExtent ] = robotDef . extents
660+ const [ backLeftBound , frontRightBound ] = boundingBox
661+ const { x : pipetteLeftBound , y : pipetteBackBound } = backLeftBound
662+ const { x : pipetteRightBound , y : pipetteFrontBound } = frontRightBound
663+
664+ if ( channels === 96 ) {
665+ // check left
666+ if ( pipetteRightBound < leftSide ) {
667+ return false
668+ }
669+ // check right
670+ const rightLimit = xExtent + rightSide
671+ if ( pipetteLeftBound > rightLimit ) {
672+ return false
673+ }
674+ }
675+
676+ // 8- and 96-channel pipettes
677+ if ( channels !== 1 ) {
678+ // check front
679+ if ( pipetteBackBound < front ) {
680+ return false
681+ }
682+ // check rear
683+ const rearLimit = yExtent + rear
684+ if ( pipetteFrontBound > rearLimit ) {
685+ return false
686+ }
687+ }
688+ return true
689+ }
0 commit comments