Skip to content

Commit adda529

Browse files
fix(app): "Move labware" animation: fix certain labware positions and prepare for labware schema 3 (#18885)
1 parent 0e72668 commit adda529

File tree

16 files changed

+807
-183
lines changed

16 files changed

+807
-183
lines changed

app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export function TwoColLwInfoAndDeck(
3030
deckMapUtils,
3131
currentRecoveryOptionUtils,
3232
isOnDevice,
33+
allRunDefs,
3334
} = props
3435
const {
3536
RETRY_NEW_TIPS,
@@ -138,6 +139,7 @@ export function TwoColLwInfoAndDeck(
138139
initialLabwareLocation={currentLoc}
139140
finalLabwareLocation={newLoc}
140141
movedLabwareDef={movedLabwareDef}
142+
labwareDefinitions={allRunDefs}
141143
{...restUtils}
142144
backgroundItems={
143145
<>

app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export function MoveLabwareInterventionContent({
197197
initialLabwareLocation={oldLabwareLocation}
198198
finalLabwareLocation={command.params.newLocation}
199199
movedLabwareDef={movedLabwareDef}
200+
labwareDefinitions={Object.values(labwareDefsByUri)}
200201
loadedModules={run.modules}
201202
loadedLabware={run.labware}
202203
deckConfig={deckConfig}

components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx

Lines changed: 120 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,17 @@ import { animated, easings, useSpring } from '@react-spring/web'
33
import styled from 'styled-components'
44

55
import {
6+
computeLabwareOrigin,
67
getDeckDefFromRobotType,
7-
getModuleDef,
8-
getPositionFromSlotId,
9-
getSchema2CornerOffsetFromSlot,
10-
getSchema2Dimensions,
8+
getLabwareViewBox,
119
} from '@opentrons/shared-data'
1210

1311
import { COLORS } from '../../helix-design-system'
1412
import { BaseDeck } from '../BaseDeck'
1513
import { LabwareRender } from '../Labware'
16-
import { IDENTITY_AFFINE_TRANSFORM, multiplyMatrices } from '../utils'
14+
import { resolveLabwareLocation } from './resolveLabwareLocation'
1715

18-
import type { ReactNode } from 'react'
16+
import type { PropsWithChildren, ReactNode } from 'react'
1917
import type {
2018
DeckConfiguration,
2119
DeckDefinition,
@@ -28,109 +26,6 @@ import type {
2826
} from '@opentrons/shared-data'
2927
import type { StyleProps } from '../../primitives'
3028

31-
const getModulePosition = (
32-
deckDef: DeckDefinition,
33-
moduleId: string,
34-
loadedModules: LoadedModule[]
35-
): Vector3D | null => {
36-
const loadedModule = loadedModules.find(m => m.id === moduleId)
37-
if (loadedModule == null) return null
38-
const modSlot = deckDef.locations.addressableAreas.find(
39-
s => s.id === loadedModule.location.slotName
40-
)
41-
if (modSlot == null) return null
42-
43-
const modPosition = getPositionFromSlotId(modSlot.id, deckDef)
44-
if (modPosition == null) return null
45-
const [modX, modY] = modPosition
46-
47-
const deckSpecificAffineTransform =
48-
getModuleDef(loadedModule.model).slotTransforms?.[deckDef.otId]?.[
49-
modSlot.id
50-
]?.labwareOffset ?? IDENTITY_AFFINE_TRANSFORM
51-
const [[labwareX], [labwareY], [labwareZ]] = multiplyMatrices(
52-
[[modX], [modY], [1], [1]],
53-
deckSpecificAffineTransform
54-
)
55-
return { x: labwareX, y: labwareY, z: labwareZ }
56-
}
57-
58-
function getLabwareCoordinates({
59-
deckDef,
60-
location,
61-
loadedModules,
62-
loadedLabware,
63-
}: {
64-
deckDef: DeckDefinition
65-
location: LabwareLocation
66-
loadedModules: LoadedModule[]
67-
loadedLabware: LoadedLabware[]
68-
}): Vector3D | null {
69-
if (location === 'offDeck' || location === 'systemLocation') {
70-
return null
71-
} else if ('labwareId' in location) {
72-
const loadedAdapter = loadedLabware.find(l => l.id === location.labwareId)
73-
if (loadedAdapter == null) return null
74-
const loadedAdapterLocation = loadedAdapter.location
75-
76-
if (
77-
loadedAdapterLocation === 'offDeck' ||
78-
loadedAdapterLocation === 'systemLocation' ||
79-
'labwareId' in loadedAdapterLocation
80-
)
81-
return null
82-
// adapter on module
83-
if ('moduleId' in loadedAdapterLocation) {
84-
return getModulePosition(
85-
deckDef,
86-
loadedAdapterLocation.moduleId,
87-
loadedModules
88-
)
89-
}
90-
91-
// adapter on deck
92-
const loadedAdapterSlotPosition = getPositionFromSlotId(
93-
'slotName' in loadedAdapterLocation
94-
? loadedAdapterLocation.slotName
95-
: loadedAdapterLocation.addressableAreaName,
96-
deckDef
97-
)
98-
return loadedAdapterSlotPosition != null
99-
? {
100-
x: loadedAdapterSlotPosition[0],
101-
y: loadedAdapterSlotPosition[1],
102-
z: loadedAdapterSlotPosition[2],
103-
}
104-
: null
105-
} else if ('addressableAreaName' in location) {
106-
const slotCoordinateTuple = getPositionFromSlotId(
107-
location.addressableAreaName,
108-
deckDef
109-
)
110-
return slotCoordinateTuple != null
111-
? {
112-
x: slotCoordinateTuple[0],
113-
y: slotCoordinateTuple[1],
114-
z: slotCoordinateTuple[2],
115-
}
116-
: null
117-
} else if ('slotName' in location) {
118-
const slotCoordinateTuple = getPositionFromSlotId(
119-
location.slotName,
120-
deckDef
121-
)
122-
return slotCoordinateTuple != null
123-
? {
124-
x: slotCoordinateTuple[0],
125-
y: slotCoordinateTuple[1],
126-
z: slotCoordinateTuple[2],
127-
}
128-
: null
129-
} else {
130-
return getModulePosition(deckDef, location.moduleId, loadedModules)
131-
}
132-
}
133-
13429
const SPLASH_Y_BUFFER_MM = 10
13530

13631
interface MoveLabwareOnDeckProps extends StyleProps {
@@ -140,6 +35,7 @@ interface MoveLabwareOnDeckProps extends StyleProps {
14035
finalLabwareLocation: LabwareLocation
14136
loadedModules: LoadedModule[]
14237
loadedLabware: LoadedLabware[]
38+
labwareDefinitions: LabwareDefinition[]
14339
deckConfig: DeckConfiguration
14440
backgroundItems?: ReactNode
14541
deckFill?: string
@@ -151,6 +47,7 @@ export function MoveLabwareOnDeck(
15147
robotType,
15248
movedLabwareDef,
15349
loadedLabware,
50+
labwareDefinitions,
15451
initialLabwareLocation,
15552
finalLabwareLocation,
15653
loadedModules,
@@ -160,69 +57,82 @@ export function MoveLabwareOnDeck(
16057
} = props
16158
const deckDef = useMemo(() => getDeckDefFromRobotType(robotType), [robotType])
16259

163-
const initialSlotId =
164-
initialLabwareLocation === 'offDeck' ||
165-
initialLabwareLocation === 'systemLocation' ||
166-
!('slotName' in initialLabwareLocation)
167-
? deckDef.locations.addressableAreas[1].id
168-
: initialLabwareLocation.slotName
169-
170-
const slotPosition = getPositionFromSlotId(initialSlotId, deckDef) ?? [
171-
0,
172-
0,
173-
0,
174-
]
175-
176-
const cornerOffsetFromSlot = getSchema2CornerOffsetFromSlot(movedLabwareDef)
60+
const initialResolvedLocation = resolveLabwareLocation({
61+
deckDef,
62+
targetLabwareDef: movedLabwareDef,
63+
targetLabwareLocation: initialLabwareLocation,
64+
loadedModules,
65+
otherLoadedLabware: loadedLabware,
66+
otherLabwareDefinitions: labwareDefinitions,
67+
})
68+
const finalResolvedLocation = resolveLabwareLocation({
69+
deckDef,
70+
targetLabwareDef: movedLabwareDef,
71+
targetLabwareLocation: finalLabwareLocation,
72+
loadedModules,
73+
otherLoadedLabware: loadedLabware,
74+
otherLabwareDefinitions: labwareDefinitions,
75+
})
17776

178-
const offDeckPosition = {
179-
x: slotPosition[0],
180-
y:
181-
deckDef.cornerOffsetFromOrigin[1] -
182-
getSchema2Dimensions(movedLabwareDef).xDimension -
183-
SPLASH_Y_BUFFER_MM,
184-
}
185-
const initialPosition =
186-
getLabwareCoordinates({
187-
deckDef,
188-
location: initialLabwareLocation,
189-
loadedModules,
190-
loadedLabware,
191-
}) ?? offDeckPosition
192-
const finalPosition =
193-
getLabwareCoordinates({
194-
deckDef,
195-
location: finalLabwareLocation,
196-
loadedModules,
197-
loadedLabware,
198-
}) ?? offDeckPosition
77+
const initialCoordinates =
78+
initialResolvedLocation === 'error' || initialResolvedLocation === 'offDeck'
79+
? initialResolvedLocation
80+
: computeLabwareOrigin(initialResolvedLocation) ?? 'error'
81+
const finalCoordinates =
82+
finalResolvedLocation === 'error' || finalResolvedLocation === 'offDeck'
83+
? finalResolvedLocation
84+
: computeLabwareOrigin(finalResolvedLocation) ?? 'error'
85+
86+
const referenceForOffDeckCoordinates = (() => {
87+
if (initialCoordinates !== 'error' && initialCoordinates !== 'offDeck') {
88+
return initialCoordinates
89+
} else if (finalCoordinates !== 'error' && finalCoordinates !== 'offDeck') {
90+
return finalCoordinates
91+
} else {
92+
return { x: 0, y: 0, z: 0 }
93+
}
94+
})()
95+
const offDeckCoordinates = getOffDeckCoordinates(
96+
deckDef,
97+
movedLabwareDef,
98+
referenceForOffDeckCoordinates,
99+
SPLASH_Y_BUFFER_MM
100+
)
199101

200-
const shouldReset = usePositionChangeReset(initialPosition, finalPosition)
102+
const animationInitialCoordinates =
103+
initialCoordinates !== 'error' && initialCoordinates !== 'offDeck'
104+
? initialCoordinates
105+
: offDeckCoordinates
106+
const animationFinalCoordinates =
107+
finalCoordinates !== 'error' && finalCoordinates !== 'offDeck'
108+
? finalCoordinates
109+
: offDeckCoordinates
110+
111+
const shouldReset = usePositionChangeReset(
112+
animationInitialCoordinates,
113+
animationFinalCoordinates
114+
)
201115

202116
const springProps = useSpring({
203117
reset: shouldReset,
204118
config: { duration: 1000, easing: easings.easeInOutSine },
205119
from: {
206-
...initialPosition,
120+
...animationInitialCoordinates,
207121
splashOpacity: 0,
208122
deckOpacity: 0,
209123
},
210124
to: [
211125
{ deckOpacity: 1 },
212126
{ splashOpacity: 1 },
213127
{ splashOpacity: 0 },
214-
{ ...finalPosition },
128+
{ ...animationFinalCoordinates },
215129
{ splashOpacity: 1 },
216130
{ splashOpacity: 0 },
217131
{ deckOpacity: 0 },
218132
],
219133
loop: true,
220134
})
221135

222-
if (deckDef == null) {
223-
return null
224-
}
225-
226136
return (
227137
<BaseDeck
228138
deckConfig={deckConfig}
@@ -235,25 +145,52 @@ export function MoveLabwareOnDeck(
235145
>
236146
{backgroundItems}
237147
<AnimatedG style={{ x: springProps.x, y: springProps.y }}>
238-
<g
239-
transform={`translate(${cornerOffsetFromSlot.x}, ${cornerOffsetFromSlot.y})`}
240-
>
241-
<LabwareRender definition={movedLabwareDef} highlight={true} />
242-
<AnimatedG style={{ opacity: springProps.splashOpacity }}>
148+
<LabwareRender
149+
definition={movedLabwareDef}
150+
positioningMode="passThrough"
151+
highlight={true}
152+
/>
153+
<AnimatedG style={{ opacity: springProps.splashOpacity }}>
154+
<AlignSplashToLabware labwareDefinition={movedLabwareDef}>
243155
<path
244156
d="M158.027 111.537L154.651 108.186M145.875 113L145.875 109.253M161 99.3038L156.864 99.3038M11.9733 10.461L15.3495 13.8128M24.1255 9L24.1254 12.747M9 22.6962L13.1357 22.6962"
245157
stroke={COLORS.blue50}
246158
strokeWidth="3.57"
247159
strokeLinecap="round"
248160
transform="scale(.97, -1) translate(-19, -104)"
249161
/>
250-
</AnimatedG>
251-
</g>
162+
</AlignSplashToLabware>
163+
</AnimatedG>
252164
</AnimatedG>
253165
</BaseDeck>
254166
)
255167
}
256168

169+
/**
170+
* Returns the coordinates of a location beyond the bounds of the deck.
171+
*
172+
* @param onDeckCoordinates - The coordinates of the labware when it's on-deck. The
173+
* off-deck location is chosen to be vertically aligned with this, so the animation
174+
* goes straight up or down.
175+
*/
176+
function getOffDeckCoordinates(
177+
deckDefinition: DeckDefinition,
178+
labwareDefinition: LabwareDefinition,
179+
onDeckCoordinates: Vector3D,
180+
extraMargin: number
181+
): Vector3D {
182+
const labwareViewBox = getLabwareViewBox(labwareDefinition)
183+
const labwareOriginToLabwareMaxY =
184+
labwareViewBox.minY + labwareViewBox.yDimension
185+
const margin = labwareOriginToLabwareMaxY + extraMargin
186+
const deckMinY = deckDefinition.cornerOffsetFromOrigin[1]
187+
const y = deckMinY - margin - labwareOriginToLabwareMaxY
188+
return {
189+
...onDeckCoordinates,
190+
y,
191+
}
192+
}
193+
257194
function usePositionChangeReset(
258195
initialPosition: { x: number; y: number },
259196
finalPosition: { x: number; y: number }
@@ -285,6 +222,31 @@ function usePositionChangeReset(
285222

286223
return shouldReset
287224
}
225+
226+
/**
227+
* The splash SVG is made for its origin to be placed at the -x,-y corner of a labware
228+
* (or the slot that the labware is in). If this component is placed at the labware
229+
* origin and the splash is placed inside this component, it will align the splash
230+
* accordingly.
231+
*/
232+
function AlignSplashToLabware(
233+
props: PropsWithChildren<{ labwareDefinition: LabwareDefinition }>
234+
): JSX.Element {
235+
const { labwareDefinition, children } = props
236+
const labwareViewBox = getLabwareViewBox(labwareDefinition)
237+
const labwareOriginToFrontLeftCorner = {
238+
x: labwareViewBox.minX,
239+
y: labwareViewBox.minY,
240+
}
241+
return (
242+
<g
243+
transform={`translate(${labwareOriginToFrontLeftCorner.x} ${labwareOriginToFrontLeftCorner.y})`}
244+
>
245+
{children}
246+
</g>
247+
)
248+
}
249+
288250
/**
289251
* These animated components needs to be split out because react-spring and styled-components don't play nice
290252
* @see https://github.com/pmndrs/react-spring/issues/1515 */

0 commit comments

Comments
 (0)