diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js index 735ca95a7c9..12f68189207 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js @@ -26,7 +26,6 @@ type TransferLikeStepArgs = ConsolidateFormData | DistributeFormData | TransferF // TODO: BC 2018-10-30 move getting labwareDef into hydration layer upstream const transferLikeFormToArgs = (hydratedFormData: FormData): TransferLikeStepArgs => { - console.log([hydratedFormData]) const stepType = hydratedFormData.stepType const pipette = hydratedFormData['pipette'] const volume = Number(hydratedFormData['volume']) diff --git a/protocol-designer/src/steplist/generateSubsteps.js b/protocol-designer/src/steplist/generateSubsteps.js index 7aca9932a07..087c3fea9c7 100644 --- a/protocol-designer/src/steplist/generateSubsteps.js +++ b/protocol-designer/src/steplist/generateSubsteps.js @@ -139,7 +139,9 @@ function transferLikeSubsteps (args: { preIngreds: nextMultiRow.dest.preIngreds[destChannelWell], postIngreds: nextMultiRow.dest.postIngreds[destChannelWell], } + const activeTips = currentMultiRow.activeTips return { + activeTips, source, dest: stepArgs.stepType === 'mix' ? source : dest, // NOTE: since source and dest are same for mix, we're showing source on both sides. Otherwise dest would show the intermediate volume state volume: showDispenseVol ? nextMultiRow.volume : currentMultiRow.volume, @@ -158,7 +160,8 @@ function transferLikeSubsteps (args: { preIngreds: currentMultiRow.dest.preIngreds[currentMultiRow.dest.wells[channelIndex]], postIngreds: currentMultiRow.dest.postIngreds[currentMultiRow.dest.wells[channelIndex]], } - return { source, dest, volume: currentMultiRow.volume } + const activeTips = currentMultiRow.activeTips + return {activeTips, source, dest, volume: currentMultiRow.volume} }) ) ) @@ -204,7 +207,12 @@ function transferLikeSubsteps (args: { preIngreds: currentRow.dest.preIngreds, postIngreds: currentRow.dest.postIngreds, } - return {source, dest, volume: currentRow.volume} + return { + activeTips: currentRow.activeTips, + source, + dest, + volume: currentRow.volume, + } } ) diff --git a/protocol-designer/src/steplist/substepTimeline.js b/protocol-designer/src/steplist/substepTimeline.js index 75244782dd2..03dd745a871 100644 --- a/protocol-designer/src/steplist/substepTimeline.js +++ b/protocol-designer/src/steplist/substepTimeline.js @@ -1,47 +1,86 @@ // @flow +import assert from 'assert' +import last from 'lodash/last' import pick from 'lodash/pick' import type {Channels} from '@opentrons/components' -import type {CommandCreator, RobotState, CommandCreatorError} from '../step-generation/types' +import type { + CommandCreator, + CommandCreatorError, + CommandsAndRobotState, + RobotState, +} from '../step-generation/types' import {getWellsForTips} from '../step-generation/utils' -import type {SubstepTimelineFrame} from './types' +import type {SubstepTimelineFrame, TipLocation} from './types' -type SubstepTimeline = { +function _conditionallyUpdateActiveTips (acc: SubstepTimelineAcc, nextFrame: CommandsAndRobotState) { + const lastNewTipCommand = last(nextFrame.commands.filter(c => c.command === 'pick-up-tip')) + const newTipParams = lastNewTipCommand && + lastNewTipCommand.command === 'pick-up-tip' && + lastNewTipCommand.params + + if (newTipParams) { + return {...acc, prevActiveTips: {...newTipParams}} + } + return acc +} + +type SubstepTimelineAcc = { timeline: Array, errors: ?Array, + prevActiveTips: ?TipLocation, + prevRobotState: RobotState, } + const substepTimelineSingle = (commandCreators: Array) => (initialRobotState: RobotState): Array => { - let prevRobotState = initialRobotState - const timeline = commandCreators.reduce((acc: SubstepTimeline, commandCreator: CommandCreator, index: number) => { + const timeline = commandCreators.reduce((acc: SubstepTimelineAcc, commandCreator: CommandCreator, index: number) => { // error short-circuit if (acc.errors) return acc - const nextFrame = commandCreator(prevRobotState) + const nextFrame = commandCreator(acc.prevRobotState) if (nextFrame.errors) { - return {timeline: acc.timeline, errors: nextFrame.errors} + return {...acc, errors: nextFrame.errors} } // NOTE: only aspirate and dispense commands will appear alone in atomic commands // from compound command creators (e.g. transfer, distribute, etc.) - const isAtomic = nextFrame.commands.length === 1 - const commandGroup = nextFrame.commands[0] - if (isAtomic && (commandGroup.command === 'aspirate' || commandGroup.command === 'dispense')) { + const firstCommand = nextFrame.commands[0] + if ( + firstCommand.command === 'aspirate' || + firstCommand.command === 'dispense') { + assert(nextFrame.commands.length === 1, + `substepTimeline expected nextFrame to have only single commands for ${firstCommand.command}`) + + const commandGroup = firstCommand const {well, volume, labware} = commandGroup.params const wellInfo = { labware, wells: [well], - preIngreds: prevRobotState.liquidState.labware[labware][well], + preIngreds: acc.prevRobotState.liquidState.labware[labware][well], postIngreds: nextFrame.robotState.liquidState.labware[labware][well], } - prevRobotState = nextFrame.robotState const ingredKey = commandGroup.command === 'aspirate' ? 'source' : 'dest' - return {timeline: [...acc.timeline, {volume, [ingredKey]: wellInfo}], errors: null} + return { + ...acc, + timeline: [ + ...acc.timeline, + { + volume, + [ingredKey]: wellInfo, + activeTips: acc.prevActiveTips, + }, + ], + prevRobotState: nextFrame.robotState, + } } else { - return acc + return { + ..._conditionallyUpdateActiveTips(acc, nextFrame), + prevRobotState: nextFrame.robotState, + } } - }, {timeline: [], errors: null}) + }, {timeline: [], errors: null, prevActiveTips: null, prevRobotState: initialRobotState}) return timeline.timeline } @@ -54,38 +93,57 @@ const substepTimeline = ( if (context.channels === 1) { return substepTimelineSingle(commandCreators) } else { + // timeline for multi-channel substep context return ( (initialRobotState: RobotState): Array => { - let prevRobotState = initialRobotState - const timeline = commandCreators.reduce((acc: SubstepTimeline, commandCreator: CommandCreator, index: number) => { + const timeline = commandCreators.reduce((acc: SubstepTimelineAcc, commandCreator: CommandCreator, index: number) => { // error short-circuit if (acc.errors) return acc - const nextFrame = commandCreator(prevRobotState) + const nextFrame = commandCreator(acc.prevRobotState) if (nextFrame.errors) { - return {timeline: acc.timeline, errors: nextFrame.errors} + return {...acc, errors: nextFrame.errors} } - const isAtomic = nextFrame.commands.length === 1 - const commandGroup = nextFrame.commands[0] - if (isAtomic && (commandGroup.command === 'aspirate' || commandGroup.command === 'dispense')) { - const {well, volume, labware} = commandGroup.params + const firstCommand = nextFrame.commands[0] + if ( + firstCommand.command === 'aspirate' || + firstCommand.command === 'dispense' + ) { + assert(nextFrame.commands.length === 1, + `substepTimeline expected nextFrame to have only single commands for ${firstCommand.command}`) + + const {well, volume, labware} = firstCommand.params const labwareType = context.getLabwareType && context.getLabwareType(labware) const wellsForTips = context.channels && labwareType && getWellsForTips(context.channels, labwareType, well).wellsForTips const wellInfo = { labware, wells: wellsForTips || [], - preIngreds: wellsForTips ? pick(prevRobotState.liquidState.labware[labware], wellsForTips) : {}, + preIngreds: wellsForTips ? pick(acc.prevRobotState.liquidState.labware[labware], wellsForTips) : {}, postIngreds: wellsForTips ? pick(nextFrame.robotState.liquidState.labware[labware], wellsForTips) : {}, } - prevRobotState = nextFrame.robotState - const ingredKey = commandGroup.command === 'aspirate' ? 'source' : 'dest' - return {timeline: [...acc.timeline, {volume, [ingredKey]: wellInfo}], errors: null} + + const ingredKey = firstCommand.command === 'aspirate' ? 'source' : 'dest' + return { + ...acc, + timeline: [ + ...acc.timeline, + { + volume, + [ingredKey]: wellInfo, + activeTips: acc.prevActiveTips, + }, + ], + prevRobotState: nextFrame.robotState, + } } else { - return acc + return { + ..._conditionallyUpdateActiveTips(acc, nextFrame), + prevRobotState: nextFrame.robotState, + } } - }, {timeline: [], errors: null}) + }, {timeline: [], errors: null, prevActiveTips: null, prevRobotState: initialRobotState}) return timeline.timeline } diff --git a/protocol-designer/src/steplist/types.js b/protocol-designer/src/steplist/types.js index eaf8c02c9a5..6cf59e343cf 100644 --- a/protocol-designer/src/steplist/types.js +++ b/protocol-designer/src/steplist/types.js @@ -20,8 +20,8 @@ export const END_TERMINAL_ITEM_ID: '__end__' = '__end__' export type TerminalItemId = typeof START_TERMINAL_ITEM_ID | typeof END_TERMINAL_ITEM_ID export type WellIngredientNames = {[ingredId: string]: string} - export type WellIngredientVolumeData = {[ingredId: string]: {volume: number}} +export type TipLocation = {labware: string, well: string} export type SubstepIdentifier = {| stepId: StepIdType, @@ -41,6 +41,7 @@ export type SourceDestData = { export type SubstepTimelineFrame = { substepIndex?: number, + activeTips: ?TipLocation, source?: SourceDestData, dest?: SourceDestData, volume?: ?number, @@ -54,6 +55,7 @@ export type SubstepWellData = { } export type StepItemSourceDestRow = { + activeTips: ?TipLocation, substepIndex?: number, source?: SubstepWellData, dest?: SubstepWellData, diff --git a/protocol-designer/src/top-selectors/substeps.js b/protocol-designer/src/top-selectors/substeps.js index 48dddb81986..23948d480e2 100644 --- a/protocol-designer/src/top-selectors/substeps.js +++ b/protocol-designer/src/top-selectors/substeps.js @@ -30,8 +30,9 @@ export const allSubsteps: Selector = createSelector( robotStateTimeline, _initialRobotState, ) => { + const timeline = [{robotState: _initialRobotState}, ...robotStateTimeline.timeline] + return orderedSteps.reduce((acc: AllSubsteps, stepId, timelineIndex) => { - const timeline = [{robotState: _initialRobotState}, ...robotStateTimeline.timeline] const robotState = timeline[timelineIndex] && timeline[timelineIndex].robotState const substeps = generateSubsteps( diff --git a/protocol-designer/src/top-selectors/tip-contents/index.js b/protocol-designer/src/top-selectors/tip-contents/index.js index a131261c814..9a609c5d9af 100644 --- a/protocol-designer/src/top-selectors/tip-contents/index.js +++ b/protocol-designer/src/top-selectors/tip-contents/index.js @@ -2,6 +2,7 @@ import {createSelector} from 'reselect' import noop from 'lodash/noop' import * as StepGeneration from '../../step-generation' +import {allSubsteps as getAllSubsteps} from '../substeps' import { selectors as steplistSelectors, START_TERMINAL_ITEM_ID, @@ -32,16 +33,16 @@ function getTipHighlighted ( if (c.command === 'pick-up-tip' && c.params.labware === labwareId) { const commandWellName = c.params.well const pipetteId = c.params.pipette - const labwareName = StepGeneration.getLabwareType(labwareId, robotState) + const labwareType = StepGeneration.getLabwareType(labwareId, robotState) const channels = StepGeneration.getPipetteChannels(pipetteId, robotState) - if (!labwareName) { - console.error(`Labware ${labwareId} missing labwareName. Could not get tip highlight state`) + if (!labwareType) { + console.error(`Labware ${labwareId} missing labwareType. Could not get tip highlight state`) return false } else if (channels === 1) { return commandWellName === wellName } else if (channels === 8) { - const wellSet = getWellSetForMultichannel(labwareName, commandWellName) + const wellSet = getWellSetForMultichannel(labwareType, commandWellName) return Boolean(wellSet && wellSet.includes(wellName)) } else { console.error(`Unexpected number of channels: ${channels || '?'}. Could not get tip highlight state`) @@ -93,7 +94,9 @@ export const getTipsForCurrentStep: GetTipSelector = createSelector( getInitialTips, getLastValidTips, getLabwareIdProp, - (orderedSteps, robotStateTimeline, hoveredStepId, activeItem, initialTips, lastValidTips, labwareId) => { + steplistSelectors.getHoveredSubstep, + getAllSubsteps, + (orderedSteps, robotStateTimeline, hoveredStepId, activeItem, initialTips, lastValidTips, labwareId, hoveredSubstepIdentifier, allSubsteps) => { if (!activeItem.isStep) { const terminalId = activeItem.id if (terminalId === START_TERMINAL_ITEM_ID) { @@ -128,9 +131,39 @@ export const getTipsForCurrentStep: GetTipSelector = createSelector( : false // show highlights of tips used by current frame, if user is hovering - const highlighted = (hovered && currentFrame) - ? getTipHighlighted(labwareId, wellName, currentFrame) - : false + let highlighted = false + if (hoveredSubstepIdentifier && currentFrame) { + const {substepIndex} = hoveredSubstepIdentifier + const substepsForStep = allSubsteps[hoveredSubstepIdentifier.stepId] + + if (substepsForStep && substepsForStep.stepType !== 'pause') { + if (substepsForStep.multichannel) { + const hoveredSubstepData = substepsForStep.multiRows[substepIndex][0] // just use first multi row + + const labwareType = StepGeneration.getLabwareType(labwareId, currentFrame.robotState) + const wellSet = (labwareType && hoveredSubstepData.activeTips) + ? getWellSetForMultichannel(labwareType, hoveredSubstepData.activeTips.well) + : [] + + highlighted = (hoveredSubstepData && + hoveredSubstepData.activeTips && + hoveredSubstepData.activeTips.labware === labwareId && + Boolean(wellSet && wellSet.includes(wellName)) + ) || false + } else { + // single-channel + const hoveredSubstepData = substepsForStep.rows[substepIndex] + + highlighted = (hoveredSubstepData && + hoveredSubstepData.activeTips && + hoveredSubstepData.activeTips.labware === labwareId && + hoveredSubstepData.activeTips.well === wellName + ) || false + } + } + } else if (hovered && currentFrame) { + highlighted = getTipHighlighted(labwareId, wellName, currentFrame) + } return { empty,