Skip to content

Commit

Permalink
feat(protocol-designer, step-generation, components, shared-data): ad…
Browse files Browse the repository at this point in the history
…d flex and move labware (#12920)

Introduce a new file creation wizard within protocol designer that sequentially walks users through
initializing a protocol for either the OT2 or the Opentrons Flex. Plug these new values into the
rest of the applications state, and follow the path all the way to the analysis/protocol engine via
updates to step generation to handle moving labware and speccing a robot type inside of a JSON
protocol file. Use modern module visualization components for rendering deck map items now that the
deck definition is variable within protocol designer. Render timeline module state visually on the
module svg with a new generic module name label. Remove step-generation error handling for deck
collisions that don't apply to the Flex. Present the Magnetic Block on the File Info Modules
section.

Co-authored-by: Jethary <[email protected]>
  • Loading branch information
b-cooper and jerader authored Jun 16, 2023
1 parent e9889b4 commit 10923bf
Show file tree
Hide file tree
Showing 115 changed files with 9,474 additions and 2,009 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@ exports[`InstrumentDiagram 96-channel GEN1 renders correctly 1`] = `
exports[`InstrumentDiagram Multi-channel GEN2 renders correctly 1`] = `
.c0 {
min-width: 0;
-webkit-transform: scaleX(-1);
-ms-transform: scaleX(-1);
transform: scaleX(-1);
-webkit-filter: FlipH;
filter: FlipH;
}
<div
className="c0"
>
<img
className="flipped_image"
className=""
src="multi-channel_GEN2_800px.png"
/>
</div>
Expand All @@ -48,13 +53,18 @@ exports[`InstrumentDiagram Multi-channel renders correctly 1`] = `
exports[`InstrumentDiagram Single-channel GEN2 renders correctly 1`] = `
.c0 {
min-width: 0;
-webkit-transform: scaleX(-1);
-ms-transform: scaleX(-1);
transform: scaleX(-1);
-webkit-filter: FlipH;
filter: FlipH;
}
<div
className="c0"
>
<img
className="flipped_image"
className=""
src="single-channel_GEN2_800px.png"
/>
</div>
Expand All @@ -78,13 +88,18 @@ exports[`InstrumentDiagram Single-channel renders correctly 1`] = `
exports[`InstrumentDiagram eight-channel GEN3 renders correctly 1`] = `
.c0 {
min-width: 0;
-webkit-transform: scaleX(-1);
-ms-transform: scaleX(-1);
transform: scaleX(-1);
-webkit-filter: FlipH;
filter: FlipH;
}
<div
className="c0"
>
<img
className="flipped_image"
className=""
src="eight-channel-gen3.png"
/>
</div>
Expand All @@ -93,13 +108,18 @@ exports[`InstrumentDiagram eight-channel GEN3 renders correctly 1`] = `
exports[`InstrumentDiagram single-channel GEN3 renders correctly 1`] = `
.c0 {
min-width: 0;
-webkit-transform: scaleX(-1);
-ms-transform: scaleX(-1);
transform: scaleX(-1);
-webkit-filter: FlipH;
filter: FlipH;
}
<div
className="c0"
>
<img
className="flipped_image"
className=""
src="single-channel-gen3.png"
/>
</div>
Expand All @@ -110,6 +130,15 @@ exports[`InstrumentGroup Renders correctly 1`] = `
min-width: 0;
}
.c1 {
min-width: 0;
-webkit-transform: scaleX(-1);
-ms-transform: scaleX(-1);
transform: scaleX(-1);
-webkit-filter: FlipH;
filter: FlipH;
}
<section
className="pipette_group"
>
Expand Down Expand Up @@ -161,10 +190,10 @@ exports[`InstrumentGroup Renders correctly 1`] = `
</div>
</div>
<div
className="c0 pipette_icon"
className="c1 pipette_icon"
>
<img
className="flipped_image"
className=""
src="single_channel_GEN1_800px.png"
/>
</div>
Expand Down
47 changes: 47 additions & 0 deletions components/src/atoms/StepMeter/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react'
import { css } from 'styled-components'
import { Box } from '../../primitives'
import { COLORS, RESPONSIVENESS, SPACING } from '../../ui-style-constants'
import { POSITION_ABSOLUTE, POSITION_RELATIVE } from '../../styles'

interface StepMeterProps {
totalSteps: number
currentStep: number | null
}

export const StepMeter = (props: StepMeterProps): JSX.Element => {
const { totalSteps, currentStep } = props
const progress = currentStep != null ? currentStep : 0
const percentComplete = `${
// this logic puts a cap at 100% percentComplete which we should never run into
currentStep != null && currentStep > totalSteps
? 100
: (progress / totalSteps) * 100
}%`

const StepMeterContainer = css`
position: ${POSITION_RELATIVE};
height: ${SPACING.spacing4};
background-color: ${COLORS.medGreyEnabled};
@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
height: ${SPACING.spacing12};
}
`
const StepMeterBar = css`
position: ${POSITION_ABSOLUTE};
top: 0;
height: 100%;
background-color: ${COLORS.blueEnabled};
width: ${percentComplete};
webkit-transition: width 0.5s ease-in-out;
moz-transition: width 0.5s ease-in-out;
o-transition: width 0.5s ease-in-out;
transition: width 0.5s ease-in-out;
`

return (
<Box data-testid="StepMeter_StepMeterContainer" css={StepMeterContainer}>
<Box data-testid="StepMeter_StepMeterBar" css={StepMeterBar} />
</Box>
)
}
1 change: 1 addition & 0 deletions components/src/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './CheckboxField'
export * from './StepMeter'
export * from './buttons'
34 changes: 32 additions & 2 deletions components/src/hardware-sim/Module/Thermocycler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import { RobotCoordsForeignDiv } from '../Deck'
import { ModuleFromDef } from './ModuleFromDef'

import { C_MED_LIGHT_GRAY } from '../../styles'
import { COLORS, BORDERS } from '../../ui-style-constants'

const ROOM_TEMPERATURE_C = 23 // value taken from TC firmware
export interface ThermocyclerVizProps {
lidMotorState: 'open' | 'closed' | 'unknown'
blockTargetTemp: number | null
}

export function Thermocycler(props: ThermocyclerVizProps): JSX.Element {
const { lidMotorState } = props
const { lidMotorState, blockTargetTemp } = props
const def = getModuleDef2(THERMOCYCLER_MODULE_V1)
if (lidMotorState === 'unknown') {
// just a rectangle if we don't know the state of the lid
Expand All @@ -26,6 +29,7 @@ export function Thermocycler(props: ThermocyclerVizProps): JSX.Element {
innerDivProps={{
borderRadius: '6px',
backgroundColor: C_MED_LIGHT_GRAY,
border: BORDERS.lineBorder,
width: '100%',
height: '100%',
}}
Expand All @@ -45,5 +49,31 @@ export function Thermocycler(props: ThermocyclerVizProps): JSX.Element {
},
[]
)
return <ModuleFromDef def={def} layerBlocklist={layerBlocklist} />
let ledLightOverlay = null
if (blockTargetTemp != null) {
ledLightOverlay = (
<RobotCoordsForeignDiv
width="100"
height="10"
x="36"
y="22"
innerDivProps={{
borderRadius: '6px',
backgroundColor:
blockTargetTemp <= ROOM_TEMPERATURE_C
? COLORS.mediumBlueEnabled
: COLORS.red4,
width: '100%',
height: '100%',
}}
/>
)
}

return (
<>
<ModuleFromDef def={def} layerBlocklist={layerBlocklist} />
{ledLightOverlay}
</>
)
}
70 changes: 64 additions & 6 deletions components/src/hardware-sim/Module/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getModuleType,
MAGNETIC_BLOCK_TYPE,
ModuleDefinition,
OT2_STANDARD_DECKID,
THERMOCYCLER_MODULE_TYPE,
} from '@opentrons/shared-data'
import {
Expand All @@ -24,6 +25,17 @@ export * from './Thermocycler'
export * from './ModuleFromDef'

const LABWARE_OFFSET_DISPLAY_THRESHOLD = 2

// multiply two matrices together (dot product)
function multiplyMatrices(a: number[][], b: number[][]): number[][] {
const transposedB = b[0].map((_val, index) => b.map(row => row[index]))
return a.map(rowA =>
transposedB.map(rowB =>
rowA.reduce((acc, valA, colIndexA) => acc + valA * rowB[colIndexA], 0)
)
)
}

interface Props {
x: number
y: number
Expand All @@ -35,6 +47,8 @@ interface Props {
| {}
statusInfo?: React.ReactNode // contents of small status rectangle, not displayed if absent
children?: React.ReactNode // contents to be rendered on top of the labware mating surface of the module
targetSlotId?: string
targetDeckId?: string
}

const statusInfoWrapperProps = {
Expand All @@ -60,11 +74,17 @@ export const Module = (props: Props): JSX.Element => {
innerProps = {},
statusInfo,
children,
targetSlotId,
targetDeckId = OT2_STANDARD_DECKID,
} = props
const moduleType = getModuleType(def.model)

const { x: labwareOffsetX, y: labwareOffsetY } = def.labwareOffset
const { x: translateX, y: translateY } = def.cornerOffsetFromSlot
const {
x: translateX,
y: translateY,
z: translateZ,
} = def.cornerOffsetFromSlot
const {
xDimension,
yDimension,
Expand All @@ -79,7 +99,45 @@ export const Module = (props: Props): JSX.Element => {

// apply translation to compensate for the offset of the overall module's
// left-bottom-front corner, from the footprint's left-bottom-front corner (slot interface)
const offsetTransform = `translate(${translateX}, ${translateY})`
let offsetTransform = `translate(${translateX}, ${translateY})`

let nestedLabwareOffsetX = labwareOffsetX
let nestedLabwareOffsetY = labwareOffsetY

// additional transforms to apply to vectors in certain deck/slot combinations
const transformsForDeckBySlot = def?.slotTransforms?.[targetDeckId]
const slotTransformsForDeckSlot =
targetSlotId != null &&
transformsForDeckBySlot != null &&
targetSlotId in transformsForDeckBySlot
? transformsForDeckBySlot[targetSlotId]
: null
const deckSpecificTransforms = slotTransformsForDeckSlot ?? {}
if (deckSpecificTransforms?.cornerOffsetFromSlot != null) {
const [
[slotTranslateX],
[slotTranslateY],
] = multiplyMatrices(deckSpecificTransforms.cornerOffsetFromSlot, [
[translateX],
[translateY],
[translateZ],
[1],
])
offsetTransform = `translate(${slotTranslateX}, ${slotTranslateY})`
}
if (deckSpecificTransforms?.labwareOffset != null) {
const [
[slotLabwareOffsetX],
[slotLabwareOffsetY],
] = multiplyMatrices(deckSpecificTransforms.labwareOffset, [
[labwareOffsetX],
[labwareOffsetY],
[1],
[1],
])
nestedLabwareOffsetX = slotLabwareOffsetX
nestedLabwareOffsetY = slotLabwareOffsetY
}

// find coordinates of center of footprint, fallback to overall center if not defined
const rotationCenterX = (footprintXDimension ?? xDimension) / 2
Expand All @@ -92,12 +150,12 @@ export const Module = (props: Props): JSX.Element => {

// labwareOffset values are more accurate than our SVG renderings, so ignore any deviations under a certain threshold
const clampedLabwareOffsetX =
Math.abs(labwareOffsetX) > LABWARE_OFFSET_DISPLAY_THRESHOLD
? labwareOffsetX
Math.abs(nestedLabwareOffsetX) > LABWARE_OFFSET_DISPLAY_THRESHOLD
? nestedLabwareOffsetX
: 0
const clampedLabwareOffsetY =
Math.abs(labwareOffsetY) > LABWARE_OFFSET_DISPLAY_THRESHOLD
? labwareOffsetY
Math.abs(nestedLabwareOffsetY) > LABWARE_OFFSET_DISPLAY_THRESHOLD
? nestedLabwareOffsetY
: 0
// transform to be applied to children which render within the labware interfacing surface of the module
const childrenTransform = `translate(${clampedLabwareOffsetX}, ${clampedLabwareOffsetY})`
Expand Down
20 changes: 11 additions & 9 deletions components/src/instrument/InstrumentDiagram.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import cx from 'classnames'
import { FlattenSimpleInterpolation } from 'styled-components'
import { Box } from '..'
import singleSrc from '@opentrons/components/src/instrument/single_channel_GEN1_800px.png'
import multiSrc from '@opentrons/components/src/instrument/multi-channel_GEN1_800px.png'
Expand All @@ -8,20 +8,20 @@ import multiGEN2Src from '@opentrons/components/src/instrument/multi-channel_GEN
import singleFlexSrc from '@opentrons/components/src/instrument/single-channel-gen3.png'
import eightChannelFlexSrc from '@opentrons/components/src/instrument/eight-channel-gen3.png'
import ninetySixSrc from '@opentrons/components/src/instrument/ninety-six-channel-gen1.png'
import styles from './instrument.css'

import type { PipetteNameSpecs } from '@opentrons/shared-data'
import type { Mount } from '../robot-types'
import type { StyleProps } from '..'

export interface InstrumentDiagramProps extends StyleProps {
mount: Mount
pipetteSpecs?: Pick<PipetteNameSpecs, 'displayCategory' | 'channels'> | null
className?: string
mount: Mount
imageStyle?: FlattenSimpleInterpolation
}

export function InstrumentDiagram(props: InstrumentDiagramProps): JSX.Element {
const { pipetteSpecs, mount, className, ...styleProps } = props
const { mount, pipetteSpecs, className, imageStyle, ...styleProps } = props
const { displayCategory, channels } = pipetteSpecs || {}

let imgSrc
Expand All @@ -41,11 +41,13 @@ export function InstrumentDiagram(props: InstrumentDiagramProps): JSX.Element {
}

return (
<Box className={className} {...styleProps}>
<img
className={cx({ [styles.flipped_image]: mount === 'right' })}
src={channels === 96 ? ninetySixSrc : imgSrc}
/>
<Box
className={className}
transform={mount === 'right' ? 'scaleX(-1)' : ''}
filter={mount === 'right' ? 'FlipH' : ''}
{...styleProps}
>
<img src={channels === 96 ? ninetySixSrc : imgSrc} css={imageStyle} />
</Box>
)
}
Loading

0 comments on commit 10923bf

Please sign in to comment.