Skip to content

Commit 8fdd161

Browse files
jeraderCarlos-fernandez
authored andcommitted
feat(protocol-designer, step-generation): custom z offset for blowout (#14793)
closes AUTH-7
1 parent b576b9c commit 8fdd161

39 files changed

+454
-124
lines changed

protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json

+2
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"dispense_touchTip_mmFromBottom": 40,
102102
"disposalVolume_checkbox": true,
103103
"disposalVolume_volume": "20",
104+
"blowout_z_offset": 0,
104105
"blowout_checkbox": false,
105106
"blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin",
106107
"preWetTip": false,
@@ -157,6 +158,7 @@
157158
"labware": "1e610d40-75c7-11ea-b42f-4b64e50f43e5:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/1",
158159
"mix_wellOrder_first": "t2b",
159160
"mix_wellOrder_second": "l2r",
161+
"blowout_z_offset": 0,
160162
"blowout_checkbox": true,
161163
"blowout_location": "8053a205-f2dc-4b1d-8d05-bf8233949e2e:trashBin",
162164
"mix_mmFromBottom": 0.5,

protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json

+1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
"dispense_touchTip_mmFromBottom": null,
136136
"disposalVolume_checkbox": true,
137137
"disposalVolume_volume": "20",
138+
"blowout_z_offset": 0,
138139
"blowout_checkbox": false,
139140
"blowout_location": "84882326-9cd3-428e-8352-89f133a1fe5d:trashBin",
140141
"preWetTip": false,

protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json

+2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@
179179
"dispense_touchTip_mmFromBottom": null,
180180
"disposalVolume_checkbox": true,
181181
"disposalVolume_volume": "100",
182+
"blowout_z_offset": 0,
182183
"blowout_checkbox": false,
183184
"blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin",
184185
"preWetTip": false,
@@ -209,6 +210,7 @@
209210
"labware": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2",
210211
"mix_wellOrder_first": "t2b",
211212
"mix_wellOrder_second": "l2r",
213+
"blowout_z_offset": 0,
212214
"blowout_checkbox": false,
213215
"blowout_location": "4824b094-5999-4549-9e6b-7098a9b30a8b:trashBin",
214216
"mix_mmFromBottom": 0.5,

protocol-designer/fixtures/protocol/8/doItAllV8.json

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"dispense_y_position": 0,
159159
"aspirate_x_position": 0,
160160
"aspirate_y_position": 0,
161+
"blowout_z_offset": 0,
161162
"id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7",
162163
"stepType": "moveLiquid",
163164
"stepName": "transfer",

protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"disposalVolume_checkbox": true,
115115
"disposalVolume_volume": "1",
116116
"blowout_checkbox": true,
117+
"blowout_z_offset": 0,
117118
"blowout_location": "9b1c0d01-9d4f-4016-afe6-9e08b46acf5e:trashBin",
118119
"preWetTip": false,
119120
"aspirate_airGap_checkbox": false,
@@ -143,6 +144,7 @@
143144
"labware": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well",
144145
"mix_wellOrder_first": "t2b",
145146
"mix_wellOrder_second": "l2r",
147+
"blowout_z_offset": 0,
146148
"blowout_checkbox": true,
147149
"blowout_location": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well",
148150
"mix_mmFromBottom": 0.5,
@@ -5695,7 +5697,7 @@
56955697
"labwareId": "dafd4000-92a5-11e9-ac62-1b173f839d9e:96-deep-well",
56965698
"wellName": "A1",
56975699
"flowRate": 7,
5698-
"wellLocation": { "origin": "bottom", "offset": { "z": 41.3 } }
5700+
"wellLocation": { "origin": "top", "offset": { "z": 0 } }
56995701
}
57005702
},
57015703
{

protocol-designer/fixtures/protocol/8/mix_8_0_0.json

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"labware": null,
5959
"mix_wellOrder_first": "t2b",
6060
"mix_wellOrder_second": "l2r",
61+
"blowout_z_offset": 0,
6162
"blowout_checkbox": false,
6263
"blowout_location": "5ba7047d-d3e2-4845-9eaa-1974af796ead:trashBin",
6364
"mix_mmFromBottom": 0.5,

protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"dispense_touchTip_mmFromBottom": null,
7979
"disposalVolume_checkbox": true,
8080
"disposalVolume_volume": "5",
81+
"blowout_z_offset": 0,
8182
"blowout_checkbox": false,
8283
"blowout_location": null,
8384
"preWetTip": false,
@@ -133,6 +134,7 @@
133134
"dispense_touchTip_mmFromBottom": null,
134135
"disposalVolume_checkbox": true,
135136
"disposalVolume_volume": "5",
137+
"blowout_z_offset": 0,
136138
"blowout_checkbox": false,
137139
"blowout_location": null,
138140
"preWetTip": false,

protocol-designer/src/components/StepEditForm/fields/BlowoutLocationField.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import styles from '../StepEditForm.module.css'
77
import { FieldProps } from '../types'
88

99
type BlowoutLocationDropdownProps = FieldProps & {
10-
className?: string
1110
options: Options
11+
className?: string
1212
}
1313

1414
export const BlowoutLocationField = (
@@ -28,7 +28,7 @@ export const BlowoutLocationField = (
2828

2929
return (
3030
<DropdownField
31-
className={cx(styles.large_field, className)}
31+
className={cx(styles.small_field, className)}
3232
options={options}
3333
disabled={disabled}
3434
id={'BlowoutLocationField_dropdown'}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as React from 'react'
2+
import { useSelector } from 'react-redux'
3+
import {
4+
DEST_WELL_BLOWOUT_DESTINATION,
5+
SOURCE_WELL_BLOWOUT_DESTINATION,
6+
} from '@opentrons/step-generation'
7+
import {
8+
COLORS,
9+
Flex,
10+
Icon,
11+
Tooltip,
12+
useHoverTooltip,
13+
} from '@opentrons/components'
14+
import { getLabwareEntities } from '../../../step-forms/selectors'
15+
import { ZTipPositionModal } from './TipPositionField/ZTipPositionModal'
16+
import type { FieldProps } from '../types'
17+
18+
interface BlowoutZOffsetFieldProps extends FieldProps {
19+
destLabwareId: unknown
20+
sourceLabwareId?: unknown
21+
blowoutLabwareId?: unknown
22+
}
23+
24+
export function BlowoutZOffsetField(
25+
props: BlowoutZOffsetFieldProps
26+
): JSX.Element {
27+
const {
28+
disabled,
29+
value,
30+
destLabwareId,
31+
sourceLabwareId,
32+
blowoutLabwareId,
33+
tooltipContent,
34+
name,
35+
updateValue,
36+
} = props
37+
const [isModalOpen, setModalOpen] = React.useState<boolean>(false)
38+
const [targetProps, tooltipProps] = useHoverTooltip()
39+
const labwareEntities = useSelector(getLabwareEntities)
40+
41+
let labwareId = null
42+
if (blowoutLabwareId === SOURCE_WELL_BLOWOUT_DESTINATION) {
43+
labwareId = sourceLabwareId
44+
} else if (blowoutLabwareId === DEST_WELL_BLOWOUT_DESTINATION) {
45+
labwareId = destLabwareId
46+
}
47+
48+
const labwareZDimension =
49+
labwareId != null
50+
? labwareEntities[String(labwareId)]?.def.dimensions.zDimension
51+
: 0
52+
53+
return (
54+
<>
55+
<Tooltip {...tooltipProps}>{tooltipContent}</Tooltip>
56+
{isModalOpen ? (
57+
<ZTipPositionModal
58+
closeModal={() => setModalOpen(false)}
59+
name={name}
60+
zValue={Number(value)}
61+
updateValue={updateValue}
62+
wellDepthMm={labwareZDimension}
63+
/>
64+
) : null}
65+
<Flex
66+
{...targetProps}
67+
onClick={disabled ? undefined : () => setModalOpen(true)}
68+
id={`BlowoutZOffsetField_${name}`}
69+
data-testid={`BlowoutZOffsetField_${name}`}
70+
>
71+
<Icon
72+
name="ot-calibrate"
73+
size="1.5rem"
74+
cursor="pointer"
75+
color={disabled ? COLORS.grey30 : COLORS.grey50}
76+
/>
77+
</Flex>
78+
</>
79+
)
80+
}

protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionInput.module.css

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
font-weight: var(--fw-semibold);
5757
color: var(--c-blue);
5858
position: absolute;
59-
right: 10px;
6059
bottom: 45px;
6160
align-self: flex-end;
6261
}

protocol-designer/src/components/StepEditForm/fields/TipPositionField/TipPositionZAxisViz.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ import styles from './TipPositionInput.module.css'
88

99
const WELL_HEIGHT_PIXELS = 145
1010
const PIXEL_DECIMALS = 2
11-
interface Props {
12-
mmFromBottom: number
11+
interface TipPositionZAxisVizProps {
1312
wellDepthMm: number
13+
mmFromBottom?: number
14+
mmFromTop?: number
1415
}
1516

16-
export const TipPositionZAxisViz = (props: Props): JSX.Element => {
17-
const fractionOfWellHeight = props.mmFromBottom / props.wellDepthMm
17+
export function TipPositionZAxisViz(
18+
props: TipPositionZAxisVizProps
19+
): JSX.Element {
20+
const { mmFromBottom, mmFromTop, wellDepthMm } = props
21+
const positionInTube = mmFromBottom ?? mmFromTop ?? 0
22+
const fractionOfWellHeight = positionInTube / wellDepthMm
1823
const pixelsFromBottom =
19-
Number(fractionOfWellHeight) * WELL_HEIGHT_PIXELS - WELL_HEIGHT_PIXELS
20-
const roundedPixelsFromBottom = round(pixelsFromBottom, PIXEL_DECIMALS)
21-
const bottomPx = props.wellDepthMm
22-
? roundedPixelsFromBottom
23-
: props.mmFromBottom - WELL_HEIGHT_PIXELS
24+
fractionOfWellHeight * WELL_HEIGHT_PIXELS -
25+
(mmFromBottom != null ? WELL_HEIGHT_PIXELS : 0)
26+
const bottomPx = round(pixelsFromBottom, PIXEL_DECIMALS)
27+
2428
return (
2529
<div className={styles.viz_wrapper}>
2630
<img

protocol-designer/src/components/StepEditForm/fields/TipPositionField/ZTipPositionModal.tsx

+53-21
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
OutlineButton,
1212
RadioGroup,
1313
} from '@opentrons/components'
14+
import { DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP } from '../../../../constants'
1415
import { getMainPagePortalEl } from '../../../portals/MainPageModalPortal'
1516
import { getIsTouchTipField } from '../../../../form-types'
1617
import { TipPositionZAxisViz } from './TipPositionZAxisViz'
@@ -24,7 +25,7 @@ import styles from './TipPositionInput.module.css'
2425

2526
interface ZTipPositionModalProps {
2627
closeModal: () => void
27-
mmFromBottom: number | null
28+
zValue: number | null
2829
name: StepFieldName
2930
updateValue: (val?: number | null) => unknown
3031
wellDepthMm: number
@@ -36,21 +37,26 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element {
3637
isIndeterminate,
3738
name,
3839
wellDepthMm,
39-
mmFromBottom,
40+
zValue,
4041
closeModal,
4142
updateValue,
4243
} = props
4344
const { t } = useTranslation(['modal', 'button'])
44-
const defaultMmFromBottom = utils.getDefaultMmFromBottom({
45-
name,
46-
wellDepthMm,
47-
})
45+
46+
const isBlowout = name === 'blowout_z_offset'
47+
const defaultMm = isBlowout
48+
? 0
49+
: utils.getDefaultMmFromBottom({
50+
name,
51+
wellDepthMm,
52+
})
4853

4954
const [value, setValue] = React.useState<string | null>(
50-
mmFromBottom === null ? null : String(mmFromBottom)
55+
zValue !== null ? String(zValue) : null
5156
)
57+
const isSetDefault = isBlowout ? zValue === 0 : zValue === null
5258
const [isDefault, setIsDefault] = React.useState<boolean>(
53-
!isIndeterminate && mmFromBottom === null
59+
!isIndeterminate && isSetDefault
5460
)
5561
// in this modal, pristinity hides the OUT_OF_BOUNDS error only.
5662
const [isPristine, setPristine] = React.useState<boolean>(true)
@@ -71,20 +77,29 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element {
7177
}
7278
}
7379
const { maxMmFromBottom, minMmFromBottom } = getMinMaxMmFromBottom()
80+
81+
// For blowout from the top of the well
82+
const minFromTop = DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP
83+
const maxFromTop = -wellDepthMm
84+
85+
const minMm = isBlowout ? maxFromTop : minMmFromBottom
86+
const maxMm = isBlowout ? minFromTop : maxMmFromBottom
87+
7488
const errors = utils.getErrors({
7589
isDefault,
76-
minMm: minMmFromBottom,
77-
maxMm: maxMmFromBottom,
90+
minMm,
91+
maxMm,
7892
value,
7993
})
8094
const hasErrors = errors.length > 0
8195
const hasVisibleErrors = isPristine
8296
? errors.includes(TOO_MANY_DECIMALS)
8397
: hasErrors
98+
8499
const errorText = utils.getErrorText({
85100
errors,
86-
minMm: maxMmFromBottom,
87-
maxMm: minMmFromBottom,
101+
minMm,
102+
maxMm,
88103
isPristine,
89104
t,
90105
})
@@ -110,13 +125,17 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element {
110125
// if string, strip non-number characters from string and cast to number
111126
const newValue =
112127
typeof newValueRaw === 'string'
113-
? newValueRaw.replace(/[^.0-9]/, '')
128+
? newValueRaw.replace(/[^-.0-9]/, '')
114129
: String(newValueRaw)
115130

116131
if (newValue === '.') {
117132
setValue('0.')
133+
} else if (newValue === '-0') {
134+
setValue('0')
118135
} else {
119-
setValue(Number(newValue) >= 0 ? newValue : '0')
136+
isBlowout
137+
? setValue(newValue)
138+
: setValue(Number(newValue) >= 0 ? newValue : '0')
120139
}
121140
}
122141

@@ -127,7 +146,7 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element {
127146
}
128147

129148
const handleIncrementDecrement = (delta: number): void => {
130-
const prevValue = value === null ? defaultMmFromBottom : Number(value)
149+
const prevValue = value === null ? defaultMm : Number(value)
131150
setIsDefault(false)
132151
handleChange(utils.roundValue(prevValue + delta))
133152
}
@@ -143,8 +162,8 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element {
143162
const TipPositionInputField = !isDefault && (
144163
<InputField
145164
caption={t('tip_position.caption', {
146-
min: minMmFromBottom,
147-
max: maxMmFromBottom,
165+
min: minMm,
166+
max: maxMm,
148167
})}
149168
className={styles.position_from_bottom_input}
150169
error={errorText}
@@ -210,9 +229,11 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element {
210229
}}
211230
options={[
212231
{
213-
name: t('tip_position.radio_button.default', {
214-
defaultMmFromBottom,
215-
}),
232+
name: isBlowout
233+
? t('tip_position.radio_button.blowout')
234+
: t('tip_position.radio_button.default', {
235+
defaultMm,
236+
}),
216237
value: 'default',
217238
},
218239
{
@@ -246,7 +267,18 @@ export function ZTipPositionModal(props: ZTipPositionModalProps): JSX.Element {
246267
) : null}
247268
<TipPositionZAxisViz
248269
mmFromBottom={
249-
value !== null ? Number(value) : defaultMmFromBottom
270+
isBlowout
271+
? undefined
272+
: value !== null
273+
? Number(value)
274+
: defaultMm
275+
}
276+
mmFromTop={
277+
isBlowout
278+
? value !== null
279+
? Number(value)
280+
: defaultMm
281+
: undefined
250282
}
251283
wellDepthMm={wellDepthMm}
252284
/>

0 commit comments

Comments
 (0)