diff --git a/app/src/components/DeckMap/index.js b/app/src/components/DeckMap/index.js index e9ae1ba3d9b..15f20b40ba1 100644 --- a/app/src/components/DeckMap/index.js +++ b/app/src/components/DeckMap/index.js @@ -74,8 +74,8 @@ function DeckMap(props: Props) { viewBox={`-46 -10 ${488} ${390}`} // TODO: put these in variables className={className} > - {({ slots }) => - map(slots, (slot: $Values, slotId) => { + {({ deckSlotsById }) => + map(deckSlotsById, (slot: $Values, slotId) => { if (!slot.matingSurfaceUnitVector) return null // if slot has no mating surface, don't render anything in it const moduleInSlot = modulesBySlot && modulesBySlot[slotId] const allLabwareInSlot = labwareBySlot && labwareBySlot[slotId] diff --git a/components/src/deck/RobotWorkSpace.js b/components/src/deck/RobotWorkSpace.js index e4e936ed272..c0bfe4d5901 100644 --- a/components/src/deck/RobotWorkSpace.js +++ b/components/src/deck/RobotWorkSpace.js @@ -14,6 +14,11 @@ type Props = { deckLayerBlacklist?: Array, } +type GetRobotCoordsFromDOMCoords = $PropertyType< + RobotWorkSpaceRenderProps, + 'getRobotCoordsFromDOMCoords' +> + function RobotWorkSpace(props: Props) { const { children, deckDef, deckLayerBlacklist = [], viewBox } = props const wrapperRef: ElementRef<*> = useRef(null) @@ -23,10 +28,7 @@ function RobotWorkSpace(props: Props) { // Until Firefox fixes this and conforms to SVG2 draft, // it will suffer from inverted y behavior (ignores css transform) // $FlowFixMe(bc, 2019-05-31): flow type svg ref - const getRobotCoordsFromDOMCoords = ( - x: number, - y: number - ): { x: number, y: number } => { + const getRobotCoordsFromDOMCoords: GetRobotCoordsFromDOMCoords = (x, y) => { if (!wrapperRef.current) return { x: 0, y: 0 } const cursorPoint = wrapperRef.current.createSVGPoint() @@ -40,12 +42,12 @@ function RobotWorkSpace(props: Props) { if (!deckDef && !viewBox) return null let wholeDeckViewBox = null - let slots = {} + let deckSlotsById = {} if (deckDef) { const [viewBoxOriginX, viewBoxOriginY] = deckDef.cornerOffsetFromOrigin const [deckXDimension, deckYDimension] = deckDef.dimensions - slots = deckDef.locations.orderedSlots.reduce( + deckSlotsById = deckDef.locations.orderedSlots.reduce( (acc, deckSlot) => ({ ...acc, [deckSlot.id]: deckSlot }), {} ) @@ -61,7 +63,7 @@ function RobotWorkSpace(props: Props) { {deckDef && ( )} - {children && children({ slots, getRobotCoordsFromDOMCoords })} + {children && children({ deckSlotsById, getRobotCoordsFromDOMCoords })} ) } diff --git a/components/src/deck/RobotWorkSpace.md b/components/src/deck/RobotWorkSpace.md index 538422c7845..f11187e74ca 100644 --- a/components/src/deck/RobotWorkSpace.md +++ b/components/src/deck/RobotWorkSpace.md @@ -6,27 +6,27 @@ const deckDef = getDeckDefinitions()['ot2_standard'] const slotName = '5' const divSlot = '3' ; - {({ slots }) => ( + {({ deckSlotsById }) => ( <> Some Text { x: number, y: number }, -} +export type RobotWorkSpaceRenderProps = {| + deckSlotsById: { [string]: DeckSlot }, + getRobotCoordsFromDOMCoords: (number, number) => {| x: number, y: number |}, +|} diff --git a/protocol-designer/src/components/DeckSetup/DeckSetup.js b/protocol-designer/src/components/DeckSetup/DeckSetup.js index 190f205df41..4ca20fbb2a5 100644 --- a/protocol-designer/src/components/DeckSetup/DeckSetup.js +++ b/protocol-designer/src/components/DeckSetup/DeckSetup.js @@ -1,10 +1,13 @@ // @flow import * as React from 'react' +import { useSelector } from 'react-redux' +import compact from 'lodash/compact' import values from 'lodash/values' import { useOnClickOutside, RobotWorkSpace, RobotCoordsForeignDiv, + type RobotWorkSpaceRenderProps, } from '@opentrons/components' import { getLabwareHasQuirk, @@ -14,12 +17,20 @@ import { getDeckDefinitions } from '@opentrons/components/src/deck/getDeckDefini import i18n from '../../localization' import { PSEUDO_DECK_SLOTS, SPAN7_8_10_11_SLOT } from '../../constants' import { START_TERMINAL_ITEM_ID, type TerminalItemId } from '../../steplist' -import ModuleViz from './ModuleViz' -import ModuleTag from './ModuleTag' import { getModuleVizDims, inferModuleOrientationFromSlot, } from './getModuleVizDims' + +import { selectors as featureFlagSelectors } from '../../feature-flags' + +import { BrowseLabwareModal } from '../labware' +import ModuleViz from './ModuleViz' +import ModuleTag from './ModuleTag' +import SlotWarning from './SlotWarning' +import LabwareOnDeck from './LabwareOnDeck' +import { SlotControls, LabwareControls, DragPreview } from './LabwareOverlays' + import type { InitialDeckSetup, LabwareOnDeck as LabwareOnDeckType, @@ -27,12 +38,9 @@ import type { } from '../../step-forms' import type { DeckSlot } from '../../types' -import { BrowseLabwareModal } from '../labware' -import LabwareOnDeck from './LabwareOnDeck' -import { SlotControls, LabwareControls, DragPreview } from './LabwareOverlays' import styles from './DeckSetup.css' -const deckSetupLayerBlacklist = [ +const DECK_LAYER_BLACKLIST = [ 'calibrationMarkings', 'fixedBase', 'doorStops', @@ -41,6 +49,7 @@ const deckSetupLayerBlacklist = [ 'removableDeckOutline', 'screwHoles', ] + type Props = {| selectedTerminalItemId: ?TerminalItemId, handleClickOutside?: () => mixed, @@ -48,6 +57,13 @@ type Props = {| initialDeckSetup: InitialDeckSetup, |} +type ContentsProps = {| + ...RobotWorkSpaceRenderProps, + selectedTerminalItemId: ?TerminalItemId, + initialDeckSetup: InitialDeckSetup, + showGen1MultichannelCollisionWarnings: boolean, +|} + const VIEWBOX_MIN_X = -64 const VIEWBOX_MIN_Y = -10 const VIEWBOX_WIDTH = 520 @@ -119,7 +135,173 @@ const getModuleSlotDefs = ( ) } +const DeckSetupContents = (props: ContentsProps) => { + const { + initialDeckSetup, + deckSlotsById, + getRobotCoordsFromDOMCoords, + showGen1MultichannelCollisionWarnings, + } = props + + const slotsBlockedBySpanning = getSlotsBlockedBySpanning( + props.initialDeckSetup + ) + const deckSlots: Array = values(deckSlotsById) + const moduleSlots = getModuleSlotDefs(initialDeckSetup, deckSlotsById) + // NOTE: in these arrays of slots, order affects SVG render layering + // labware can be in a module or on the deck + const labwareParentSlots: Array = [...deckSlots, ...moduleSlots] + // modules can be on the deck, including pseudo-slots (eg special 'spanning' slot for thermocycler position) + const moduleParentSlots = [...deckSlots, ...values(PSEUDO_DECK_SLOTS)] + + const allLabware: Array = Object.keys( + initialDeckSetup.labware + ).reduce((acc, labwareId) => { + const labware = initialDeckSetup.labware[labwareId] + return getLabwareHasQuirk(labware.def, 'fixedTrash') + ? acc + : [...acc, labware] + }, []) + + const allModules: Array = values(initialDeckSetup.modules) + + // NOTE: naively hard-coded to show warning north of slots 1 or 3 when occupied by any module + let multichannelWarningSlots: Array = showGen1MultichannelCollisionWarnings + ? compact([ + (allModules.some(module => module.slot === '1') && + deckSlotsById?.['4']) || + null, + (allModules.some(module => module.slot === '3') && + deckSlotsById?.['6']) || + null, + ]) + : [] + + return ( + <> + {/* all modules */} + {allModules.map(module => { + const slot = moduleParentSlots.find(slot => slot.id === module.slot) + if (!slot) { + console.warn(`no slot ${module.slot} for module ${module.id}`) + return null + } + + const [moduleX, moduleY] = slot.position + const orientation = inferModuleOrientationFromSlot(slot.id) + + return ( + + + + + ) + })} + + {/* on-deck warnings */} + {multichannelWarningSlots.map(slot => ( + + ))} + + {/* SlotControls for all empty deck + module slots */} + {labwareParentSlots + .filter( + slot => + !slotsBlockedBySpanning.includes(slot.id) && + getSlotIsEmpty(props.initialDeckSetup, slot.id) + ) + .map(slot => { + return ( + + ) + })} + + {/* all labware on deck and in modules */} + {allLabware.map(labware => { + const slot = labwareParentSlots.find(slot => slot.id === labware.slot) + if (!slot) { + console.warn(`no slot ${labware.slot} for labware ${labware.id}!`) + return null + } + return ( + + + + + + + ) + })} + + + ) +} + +const DeckInstructions = (props: {| children: React.Node |}) => ( + + {props.children} + +) + +const getHasGen1MultiChannelPipette = ( + pipettes: $PropertyType +) => { + const pipetteIds = Object.keys(pipettes) + return pipetteIds.some(pipetteId => + ['p10_multi', 'p50_multi', 'p300_multi'].includes(pipettes[pipetteId]?.name) + ) +} + const DeckSetup = (props: Props) => { + const _disableCollisionWarnings = useSelector( + featureFlagSelectors.getDisableModuleRestrictions + ) + const _hasGen1MultichannelPipette = React.useMemo( + () => getHasGen1MultiChannelPipette(props.initialDeckSetup.pipettes), + [props.initialDeckSetup.pipettes] + ) + const showGen1MultichannelCollisionWarnings = + !_disableCollisionWarnings && _hasGen1MultichannelPipette + const deckDef = React.useMemo(() => getDeckDefinitions()['ot2_standard'], []) const wrapperRef = useOnClickOutside({ onClickOutside: props.handleClickOutside, @@ -134,157 +316,31 @@ const DeckSetup = (props: Props) => { ) : null - const slotsBlockedBySpanning = getSlotsBlockedBySpanning( - props.initialDeckSetup - ) - - const deckInstructions = ( - - {headerMessage} - - ) - return (
{props.drilledDown && }
- {({ slots: deckSlotsById, getRobotCoordsFromDOMCoords }) => { - const deckSlots: Array = values(deckSlotsById) - const moduleSlots = getModuleSlotDefs( - props.initialDeckSetup, - deckSlotsById - ) - // NOTE: in these arrays of slots, order affects SVG render layering - // labware can be in a module or on the deck - const labwareParentSlots: Array = [ - ...deckSlots, - ...moduleSlots, - ] - // modules can be on the deck, including pseudo-slots (eg special 'spanning' slot for thermocycler position) - const moduleParentSlots = [ - ...deckSlots, - ...values(PSEUDO_DECK_SLOTS), - ] - - const allLabware: Array = Object.keys( - props.initialDeckSetup.labware - ).reduce((acc, labwareId) => { - const labware = props.initialDeckSetup.labware[labwareId] - return getLabwareHasQuirk(labware.def, 'fixedTrash') - ? acc - : [...acc, labware] - }, []) - - const allModules: Array = values( - props.initialDeckSetup.modules - ) - - return ( - <> - {/* all modules */} - {allModules.map(module => { - const slot = moduleParentSlots.find( - slot => slot.id === module.slot - ) - if (!slot) { - console.warn( - `no slot ${module.slot} for module ${module.id}` - ) - return null - } - - const [moduleX, moduleY] = slot.position - const orientation = inferModuleOrientationFromSlot(slot.id) - - return ( - - - - - ) - })} - - {/* SlotControls for all empty deck + module slots */} - {labwareParentSlots - .filter( - slot => - !slotsBlockedBySpanning.includes(slot.id) && - getSlotIsEmpty(props.initialDeckSetup, slot.id) - ) - .map(slot => { - return ( - - ) - })} - - {/* all labware on deck and in modules */} - {allLabware.map(labware => { - const slot = labwareParentSlots.find( - slot => slot.id === labware.slot - ) - if (!slot) { - console.warn( - `no slot ${labware.slot} for labware ${labware.id}!` - ) - return null - } - return ( - - - - - - - ) - })} - - {deckInstructions} - - - ) - }} + {({ deckSlotsById, getRobotCoordsFromDOMCoords }) => ( + <> + {headerMessage} + + + )}
diff --git a/protocol-designer/src/components/DeckSetup/SlotWarning.css b/protocol-designer/src/components/DeckSetup/SlotWarning.css new file mode 100644 index 00000000000..cd1aa8baacd --- /dev/null +++ b/protocol-designer/src/components/DeckSetup/SlotWarning.css @@ -0,0 +1,16 @@ +@import '@opentrons/components'; + +.slot_warning { + rx: 6; + fill: transparent; + stroke: var(--c-plate-bg); + stroke-width: 2; + stroke-dasharray: 8 4; +} + +.warning_text { + padding: 1.3rem 0.5rem 0 0.5rem; + font-size: var(--fs-caption); + font-weight: var(--fw-regular); + color: var(--c-med-gray); +} diff --git a/protocol-designer/src/components/DeckSetup/SlotWarning.js b/protocol-designer/src/components/DeckSetup/SlotWarning.js new file mode 100644 index 00000000000..b8c158e9c42 --- /dev/null +++ b/protocol-designer/src/components/DeckSetup/SlotWarning.js @@ -0,0 +1,46 @@ +// @flow +import * as React from 'react' +import i18n from '../../localization' +import { RobotCoordsForeignDiv } from '@opentrons/components' +import styles from './SlotWarning.css' +import type { ModuleOrientation } from '../../types' + +type Props = {| + x: number, + y: number, + xDimension: number, + yDimension: number, + orientation: ModuleOrientation, + warningType: 'gen1multichannel', // NOTE: Ian 2019-10-31 if we want more on-deck warnings, expand the type here +|} + +const OVERHANG = 60 + +const SlotWarning = (props: Props) => { + const { x, y, xDimension, yDimension, orientation, warningType } = props + const rectXOffset = orientation === 'left' ? -OVERHANG : 0 + const textXOffset = orientation === 'left' ? -1 * OVERHANG : xDimension + + return ( + + + + {i18n.t(`deck.warning.${warningType}`)} + + + ) +} + +export default SlotWarning diff --git a/protocol-designer/src/localization/en/deck.json b/protocol-designer/src/localization/en/deck.json index 113e4ade798..f8bb3a2cde8 100644 --- a/protocol-designer/src/localization/en/deck.json +++ b/protocol-designer/src/localization/en/deck.json @@ -1,4 +1,7 @@ { + "warning": { + "gen1multichannel": "No 8-Channel GEN1 access" + }, "header": { "start": "Tell the robot where labware and liquids start on the deck", "end": "Click on labware to inspect the result of your protocol"