Skip to content

Commit

Permalink
fix(app, components): Fix TC lid rendering in runRecord deck maps (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
mjhuff authored Nov 5, 2024
1 parent 7669fc2 commit de01cf6
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
getModuleDisplayName,
getModuleType,
getOccludedSlotCountForModule,
THERMOCYCLER_MODULE_V1,
THERMOCYCLER_MODULE_V2,
} from '@opentrons/shared-data'
import { getLabwareLocation } from './getLabwareLocation'

Expand Down Expand Up @@ -50,7 +52,10 @@ export function getLabwareDisplayLocation(
// Module location without adapter
else if (moduleModel != null && adapterName == null) {
if (params.detailLevel === 'slot-only') {
return t('slot', { slot_name: slotName })
return moduleModel === THERMOCYCLER_MODULE_V1 ||
moduleModel === THERMOCYCLER_MODULE_V2
? t('slot', { slot_name: 'A1+B1' })
: t('slot', { slot_name: slotName })
} else {
return isOnDevice
? `${getModuleDisplayName(moduleModel)}, ${slotName}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getRunCurrentModulesInfo,
getRunCurrentLabwareOnDeck,
getRunCurrentModulesOnDeck,
updateLabwareInModules,
} from '../useDeckMapUtils'

import type { LabwareDefinition2 } from '@opentrons/shared-data'
Expand Down Expand Up @@ -495,3 +496,161 @@ describe('getIsLabwareMatch', () => {
expect(result).toBe(false)
})
})

describe('updateLabwareInModules', () => {
const mockLabwareDef: LabwareDefinition2 = {
...(fixture96Plate as LabwareDefinition2),
metadata: {
displayName: 'Mock Labware Definition',
displayCategory: 'wellPlate',
displayVolumeUnits: 'mL',
},
}

const mockModule = {
moduleModel: 'temperatureModuleV2',
moduleLocation: { slotName: 'A1' },
innerProps: {},
nestedLabwareDef: null,
highlight: null,
} as any

const mockLabware = {
labwareDef: mockLabwareDef,
labwareLocation: { slotName: 'A1' },
slotName: 'A1',
}

it('should update module with nested labware when they share the same slot', () => {
const result = updateLabwareInModules({
runCurrentModules: [mockModule],
currentLabwareInfo: [mockLabware],
})

expect(result.updatedModules).toEqual([
{
...mockModule,
nestedLabwareDef: mockLabwareDef,
},
])
expect(result.remainingLabware).toEqual([])
})

it('should keep labware separate when slots do not match', () => {
const labwareInDifferentSlot = {
...mockLabware,
labwareLocation: { slotName: 'B1' },
slotName: 'B1',
}

const result = updateLabwareInModules({
runCurrentModules: [mockModule],
currentLabwareInfo: [labwareInDifferentSlot],
})

expect(result.updatedModules).toEqual([mockModule])
expect(result.remainingLabware).toEqual([labwareInDifferentSlot])
})

it('should handle multiple modules and labware', () => {
const mockModuleB1 = {
...mockModule,
moduleLocation: { slotName: 'B1' },
}

const labwareB1 = {
...mockLabware,
labwareLocation: { slotName: 'B1' },
slotName: 'B1',
}

const labwareC1 = {
...mockLabware,
labwareLocation: { slotName: 'C1' },
slotName: 'C1',
}

const result = updateLabwareInModules({
runCurrentModules: [mockModule, mockModuleB1],
currentLabwareInfo: [mockLabware, labwareB1, labwareC1],
})

expect(result.updatedModules).toEqual([
{
...mockModule,
nestedLabwareDef: mockLabwareDef,
},
{
...mockModuleB1,
nestedLabwareDef: mockLabwareDef,
},
])
expect(result.remainingLabware).toEqual([labwareC1])
})

it('should handle empty modules array', () => {
const result = updateLabwareInModules({
runCurrentModules: [],
currentLabwareInfo: [mockLabware],
})

expect(result.updatedModules).toEqual([])
expect(result.remainingLabware).toEqual([mockLabware])
})

it('should handle empty labware array', () => {
const result = updateLabwareInModules({
runCurrentModules: [mockModule],
currentLabwareInfo: [],
})

expect(result.updatedModules).toEqual([mockModule])
expect(result.remainingLabware).toEqual([])
})

it('should handle multiple labware in same slot, nesting only one with module', () => {
const labwareA1Second = {
...mockLabware,
labwareDef: {
...mockLabwareDef,
metadata: {
...mockLabwareDef.metadata,
displayName: 'Second Labware',
},
},
}

const result = updateLabwareInModules({
runCurrentModules: [mockModule],
currentLabwareInfo: [mockLabware, labwareA1Second],
})

expect(result.updatedModules).toEqual([
{
...mockModule,
nestedLabwareDef: mockLabwareDef,
},
])
expect(result.remainingLabware).toEqual([])
})

it('should preserve module properties when updating with nested labware', () => {
const moduleWithProperties = {
...mockModule,
innerProps: { lidMotorState: 'open' },
highlight: 'someHighlight',
}

const result = updateLabwareInModules({
runCurrentModules: [moduleWithProperties],
currentLabwareInfo: [mockLabware],
})

expect(result.updatedModules).toEqual([
{
...moduleWithProperties,
nestedLabwareDef: mockLabwareDef,
},
])
})
})
50 changes: 47 additions & 3 deletions app/src/organisms/ErrorRecoveryFlows/hooks/useDeckMapUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export function useDeckMapUtils({
const deckConfig = getSimplestDeckConfigForProtocol(protocolAnalysis)
const deckDef = getDeckDefFromRobotType(robotType)

// TODO(jh, 11-05-24): Revisit this logic along with deckmap interfaces after deck map redesign.

const currentModulesInfo = useMemo(
() =>
getRunCurrentModulesInfo({
Expand All @@ -96,12 +98,17 @@ export function useDeckMapUtils({
[runRecord, labwareDefinitionsByUri]
)

const { updatedModules, remainingLabware } = useMemo(
() => updateLabwareInModules({ runCurrentModules, currentLabwareInfo }),
[runCurrentModules, currentLabwareInfo]
)

const runCurrentLabware = useMemo(
() =>
getRunCurrentLabwareOnDeck({
failedLabwareUtils,
runRecord,
currentLabwareInfo,
currentLabwareInfo: remainingLabware,
}),
[failedLabwareUtils, currentLabwareInfo]
)
Expand Down Expand Up @@ -137,7 +144,7 @@ export function useDeckMapUtils({

return {
deckConfig,
modulesOnDeck: runCurrentModules.map(
modulesOnDeck: updatedModules.map(
({ moduleModel, moduleLocation, innerProps, nestedLabwareDef }) => ({
moduleModel,
moduleLocation,
Expand All @@ -149,7 +156,7 @@ export function useDeckMapUtils({
labwareLocation,
definition,
})),
highlightLabwareEventuallyIn: [...runCurrentModules, ...runCurrentLabware]
highlightLabwareEventuallyIn: [...updatedModules, ...runCurrentLabware]
.map(el => el.highlight)
.filter(maybeSlot => maybeSlot != null) as string[],
kind: 'intervention',
Expand Down Expand Up @@ -459,3 +466,40 @@ export function getIsLabwareMatch(
return slotLocation === slotName
}
}

// If any labware share a slot with a module, the labware should be nested within the module for rendering purposes.
// This prevents issues such as TC nested labware rendering in "B1" instead of the special-cased location.
export function updateLabwareInModules({
runCurrentModules,
currentLabwareInfo,
}: {
runCurrentModules: ReturnType<typeof getRunCurrentModulesOnDeck>
currentLabwareInfo: ReturnType<typeof getRunCurrentLabwareInfo>
}): {
updatedModules: ReturnType<typeof getRunCurrentModulesOnDeck>
remainingLabware: ReturnType<typeof getRunCurrentLabwareInfo>
} {
const usedSlots = new Set<string>()

const updatedModules = runCurrentModules.map(moduleInfo => {
const labwareInSameLoc = currentLabwareInfo.find(
lw => moduleInfo.moduleLocation.slotName === lw.slotName
)

if (labwareInSameLoc != null) {
usedSlots.add(labwareInSameLoc.slotName)
return {
...moduleInfo,
nestedLabwareDef: labwareInSameLoc.labwareDef,
}
} else {
return moduleInfo
}
})

const remainingLabware = currentLabwareInfo.filter(
lw => !usedSlots.has(lw.slotName)
)

return { updatedModules, remainingLabware }
}
60 changes: 5 additions & 55 deletions components/src/hardware-sim/Deck/MoveLabwareOnDeck.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import * as React from 'react'
import styled from 'styled-components'
import flatMap from 'lodash/flatMap'
import { animated, useSpring, easings } from '@react-spring/web'
import {
getDeckDefFromRobotType,
getModuleDef2,
getPositionFromSlotId,
} from '@opentrons/shared-data'
import { LabwareRender } from '../Labware'

import { COLORS } from '../../helix-design-system'
import { IDENTITY_AFFINE_TRANSFORM, multiplyMatrices } from '../utils'
import { BaseDeck } from '../BaseDeck'

import type {
LoadedLabware,
LabwareWell,
LoadedModule,
Coordinates,
LabwareDefinition2,
Expand Down Expand Up @@ -127,7 +126,6 @@ function getLabwareCoordinates({
}
}

const OUTLINE_THICKNESS_MM = 3
const SPLASH_Y_BUFFER_MM = 10

interface MoveLabwareOnDeckProps extends StyleProps {
Expand Down Expand Up @@ -212,7 +210,9 @@ export function MoveLabwareOnDeck(
loop: true,
})

if (deckDef == null) return null
if (deckDef == null) {
return null
}

return (
<BaseDeck
Expand All @@ -229,30 +229,7 @@ export function MoveLabwareOnDeck(
<g
transform={`translate(${movedLabwareDef.cornerOffsetFromSlot.x}, ${movedLabwareDef.cornerOffsetFromSlot.y})`}
>
<rect
x={OUTLINE_THICKNESS_MM}
y={OUTLINE_THICKNESS_MM}
strokeWidth={OUTLINE_THICKNESS_MM}
stroke={COLORS.blue50}
fill={COLORS.white}
width={
movedLabwareDef.dimensions.xDimension - 2 * OUTLINE_THICKNESS_MM
}
height={
movedLabwareDef.dimensions.yDimension - 2 * OUTLINE_THICKNESS_MM
}
rx={3 * OUTLINE_THICKNESS_MM}
/>
{flatMap(
movedLabwareDef.ordering,
(row: string[], i: number, c: string[][]) =>
row.map(wellName => (
<Well
key={wellName}
wellDef={movedLabwareDef.wells[wellName]}
/>
))
)}
<LabwareRender definition={movedLabwareDef} highlight={true} />
<AnimatedG style={{ opacity: springProps.splashOpacity }}>
<path
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"
Expand All @@ -272,30 +249,3 @@ export function MoveLabwareOnDeck(
* These animated components needs to be split out because react-spring and styled-components don't play nice
* @see https://github.com/pmndrs/react-spring/issues/1515 */
const AnimatedG = styled(animated.g as any)``

interface WellProps {
wellDef: LabwareWell
}
function Well(props: WellProps): JSX.Element {
const { wellDef } = props
const { x, y } = wellDef

return wellDef.shape === 'rectangular' ? (
<rect
fill={COLORS.white}
stroke={COLORS.black90}
x={x - wellDef.xDimension / 2}
y={y - wellDef.yDimension / 2}
width={wellDef.xDimension}
height={wellDef.yDimension}
/>
) : (
<circle
fill={COLORS.white}
stroke={COLORS.black90}
cx={x}
cy={y}
r={wellDef.diameter / 2}
/>
)
}

0 comments on commit de01cf6

Please sign in to comment.