Skip to content

Commit

Permalink
feat(components): create new LabwareStackRender component (#15842)
Browse files Browse the repository at this point in the history
# Overview

According to new modal designs for labware/adapter/module stacks on
desktop and ODD, we need to render an isometric view of each element of
the stack, maintaining the topography of each stacked element. The
isometric views are constructed by transforming and aligning the labware
SVG (top face) along with two new rects (front and left side). All three
are scaled, skewed, and rotated at 30˚ angles or its derivatives
([reference](http://jeroenhoek.nl/articles/svg-and-isometric-projection.html)).

The "bottom" labware definition is an optional prop, so we have an
affordance for rendering an isometric view of a single labware from the
same component. The user can choose to highlight both or none of the
labware in the stack, resulting in a fill of blue30.

In addition, adapters are handled in the following way:
- If the adapter is used as the top definition and there is no bottom
definition, we render the non-transformed `LabwareAdapter` svg
- if the adapter is used as the bottom definition, we render the
isometric transform of the well-less adapter face. _**NOTE**_ In this
PR, the adapter's height is maintained, but we may also consider setting
the height for a bottom adapter as a constant, rendering a
standard-height rectangular prism with well-less face.

Closes [PLAT-374](https://opentrons.atlassian.net/browse/PLAT-374),
[PLAT-377](https://opentrons.atlassian.net/browse/PLAT-377),
[PLAT-394](https://opentrons.atlassian.net/browse/PLAT-394),
[PLAT-375](https://opentrons.atlassian.net/browse/PLAT-375)
  • Loading branch information
ncdiehl11 authored Aug 5, 2024
1 parent 4500e21 commit b5bc2e7
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 6 deletions.
188 changes: 188 additions & 0 deletions components/src/hardware-sim/Labware/LabwareStackRender.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import * as React from 'react'
import { WellLabels, StaticLabware } from './labwareInternals'
import { LabwareAdapter } from './LabwareAdapter'
import { COLORS } from '../../helix-design-system'

import type { LabwareDefinition2 } from '@opentrons/shared-data'
import type { HighlightedWellLabels } from './labwareInternals/types'
import type { LabwareAdapterLoadName } from './LabwareAdapter'
import type { WellLabelOption } from '../..'

const HIGHLIGHT_COLOR = COLORS.blue30
const STROKE_WIDTH = 1
const SKEW_ANGLE_DEGREES = 30
const SKEW_ANGLE_RADIANS = (SKEW_ANGLE_DEGREES * Math.PI) / 180
const COSINE_SKEW_ANGLE = Math.cos(SKEW_ANGLE_RADIANS)

export interface LabwareStackRenderProps {
/** Labware definitions in stack to render */
definitionTop: LabwareDefinition2
/** option to highlight well labels with specified color */
highlightedWellLabels?: HighlightedWellLabels
/** highlight top labware */
highlightTop: boolean
/** highlight bottom labware if it exists */
highlightBottom: boolean
gRef?: React.RefObject<SVGGElement>
definitionBottom?: LabwareDefinition2
shouldRotateAdapterOrientation?: boolean
/** option to show well labels inside or outside of labware outline */
wellLabelOption?: WellLabelOption
}

export const LabwareStackRender = (
props: LabwareStackRenderProps
): JSX.Element => {
const {
gRef,
definitionTop,
definitionBottom,
highlightTop,
wellLabelOption,
shouldRotateAdapterOrientation,
highlightBottom = false,
} = props

const labwareLoadNameTop = definitionTop.parameters.loadName
const fillColorTop = highlightTop ? HIGHLIGHT_COLOR : COLORS.white
const fillColorBottom = highlightBottom ? HIGHLIGHT_COLOR : COLORS.white

// only one labware (top)
if (definitionBottom == null) {
const { xDimension, yDimension } = definitionTop.dimensions
const isTopAdapter = definitionTop.metadata.displayCategory === 'adapter'

return isTopAdapter ? (
// adapter render
<g
transform={
shouldRotateAdapterOrientation
? `rotate(180, ${xDimension / 2}, ${yDimension / 2})`
: 'rotate(0, 0, 0)'
}
>
<g>
<LabwareAdapter
labwareLoadName={labwareLoadNameTop as LabwareAdapterLoadName}
/>
</g>
</g>
) : (
// isometric view of labware
<svg>
<g
transform={`translate(55, 28) rotate(SKEW_ANGLE_DEGREES) skewX(-${SKEW_ANGLE_DEGREES}) scale(${COSINE_SKEW_ANGLE}, ${COSINE_SKEW_ANGLE})`}
ref={gRef}
>
<StaticLabware definition={definitionTop} fill={fillColorBottom} />
{wellLabelOption != null ? (
<WellLabels
definition={definitionTop}
wellLabelOption={wellLabelOption}
wellLabelColor={fillColorBottom}
highlightedWellLabels={props.highlightedWellLabels}
/>
) : null}
</g>
<rect
width={definitionTop.dimensions.yDimension - STROKE_WIDTH}
height={definitionTop.dimensions.zDimension - STROKE_WIDTH}
transform={`translate(55, 28) rotate(180) skewY(-${SKEW_ANGLE_DEGREES}) scale(${COSINE_SKEW_ANGLE}, ${COSINE_SKEW_ANGLE})`}
strokeWidth={STROKE_WIDTH}
stroke={COLORS.black90}
fill={fillColorTop}
/>
<rect
width={definitionTop.dimensions.xDimension - STROKE_WIDTH}
height={definitionTop.dimensions.zDimension - STROKE_WIDTH}
transform={`translate(55, 28) skewY(${SKEW_ANGLE_DEGREES}) scale(${
COSINE_SKEW_ANGLE * 0.5
}, -${COSINE_SKEW_ANGLE}) `}
strokeWidth={STROKE_WIDTH}
stroke={COLORS.black90}
fill={fillColorTop}
/>
</svg>
)
}

return (
<svg>
{/* bottom labware/adapter */}
<g
transform={`translate(55, ${
28 - definitionTop.dimensions.zDimension * 0.5 - 10
}) rotate(${SKEW_ANGLE_DEGREES}) skewX(-${SKEW_ANGLE_DEGREES}) scale(${COSINE_SKEW_ANGLE}, ${COSINE_SKEW_ANGLE})`}
ref={gRef}
fill={fillColorBottom}
>
<StaticLabware definition={definitionTop} fill={fillColorBottom} />
{wellLabelOption != null &&
definitionTop.metadata.displayCategory !== 'adapter' ? (
<WellLabels
definition={definitionTop}
wellLabelOption={wellLabelOption}
wellLabelColor={fillColorBottom}
highlightedWellLabels={props.highlightedWellLabels}
/>
) : null}
</g>
<rect
width={definitionTop.dimensions.yDimension - STROKE_WIDTH}
height={definitionTop.dimensions.zDimension - STROKE_WIDTH}
transform={`translate(55, ${
28 - definitionTop.dimensions.zDimension * 0.5 - 10
}) rotate(180) skewY(-${SKEW_ANGLE_DEGREES}) scale(${COSINE_SKEW_ANGLE}, ${COSINE_SKEW_ANGLE})`}
strokeWidth={STROKE_WIDTH}
stroke={COLORS.black90}
fill={fillColorBottom}
/>
<rect
width={definitionTop.dimensions.xDimension - STROKE_WIDTH}
height={definitionTop.dimensions.zDimension - STROKE_WIDTH}
transform={`translate(55, ${
28 - definitionTop.dimensions.zDimension * 0.5 - 10
}) skewY(${SKEW_ANGLE_DEGREES}) scale(${
COSINE_SKEW_ANGLE * 0.5
}, -${COSINE_SKEW_ANGLE}) `}
strokeWidth={STROKE_WIDTH}
stroke={COLORS.black90}
fill={fillColorBottom}
/>
{/* top labware/adapter */}
<g
transform={`translate(55, 28) rotate(${SKEW_ANGLE_DEGREES}) skewX(-${SKEW_ANGLE_DEGREES}) scale(${COSINE_SKEW_ANGLE}, ${COSINE_SKEW_ANGLE})`}
ref={gRef}
>
<StaticLabware definition={definitionTop} fill={fillColorTop} />
{wellLabelOption != null &&
definitionTop.metadata.displayCategory !== 'adapter' ? (
<WellLabels
definition={definitionTop}
wellLabelOption={wellLabelOption}
wellLabelColor={fillColorTop}
highlightedWellLabels={props.highlightedWellLabels}
/>
) : null}
</g>
<rect
width={definitionTop.dimensions.yDimension - STROKE_WIDTH}
height={definitionTop.dimensions.zDimension - STROKE_WIDTH}
transform={`translate(55, 28) rotate(180) skewY(-${SKEW_ANGLE_DEGREES}) scale(${COSINE_SKEW_ANGLE}, ${COSINE_SKEW_ANGLE})`}
strokeWidth={STROKE_WIDTH}
stroke={COLORS.black90}
fill={fillColorTop}
/>
<rect
width={definitionTop.dimensions.xDimension - STROKE_WIDTH}
height={definitionTop.dimensions.zDimension - STROKE_WIDTH}
transform={`translate(55, 28) skewY(${SKEW_ANGLE_DEGREES}) scale(${
COSINE_SKEW_ANGLE * 0.5
}, -${COSINE_SKEW_ANGLE}) `}
strokeWidth={STROKE_WIDTH}
stroke={COLORS.black90}
fill={fillColorTop}
/>
</svg>
)
}
1 change: 1 addition & 0 deletions components/src/hardware-sim/Labware/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './labwareInternals/index'
export * from './LabwareRender'
export * from './LabwareStackRender'
export * from './Labware'

export * from './labwareInternals/types'
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface LabwareOutlineProps {
highlight?: boolean
/** [legacy] override the border color */
stroke?: CSSProperties['stroke']
fill?: CSSProperties['fill']
}

const OUTLINE_THICKNESS_MM = 1
Expand All @@ -30,13 +31,19 @@ export function LabwareOutline(props: LabwareOutlineProps): JSX.Element {
isTiprack = false,
highlight = false,
stroke,
fill,
} = props
const {
parameters = { isTiprack },
dimensions = { xDimension: width, yDimension: height },
} = definition ?? {}

const backgroundFill = parameters.isTiprack ? '#CCCCCC' : COLORS.white
let backgroundFill
if (fill != null) {
backgroundFill = fill
} else {
backgroundFill = parameters.isTiprack ? '#CCCCCC' : COLORS.white
}
return (
<>
{highlight ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import flatMap from 'lodash/flatMap'

import { LabwareOutline } from './LabwareOutline'
import { Well } from './Well'
import { STYLE_BY_WELL_CONTENTS } from './StyledWells'
import { COLORS } from '../../../helix-design-system'

import type { LabwareDefinition2, LabwareWell } from '@opentrons/shared-data'
import type { WellMouseEvent } from './types'
import { STYLE_BY_WELL_CONTENTS } from './StyledWells'
import { COLORS } from '../../../helix-design-system'
import type { CSSProperties } from 'styled-components'

export interface StaticLabwareProps {
/** Labware definition to render */
Expand All @@ -22,6 +23,7 @@ export interface StaticLabwareProps {
onMouseEnterWell?: (e: WellMouseEvent) => unknown
/** Optional callback to be executed when mouse leaves a well element */
onMouseLeaveWell?: (e: WellMouseEvent) => unknown
fill?: CSSProperties['fill']
}

const TipDecoration = React.memo(function TipDecoration(props: {
Expand Down Expand Up @@ -55,13 +57,18 @@ export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element {
onLabwareClick,
onMouseEnterWell,
onMouseLeaveWell,
fill,
} = props

const { isTiprack } = definition.parameters
return (
<g onClick={onLabwareClick}>
<LabwareDetailGroup>
<LabwareOutline definition={definition} highlight={highlight} />
<LabwareOutline
definition={definition}
highlight={highlight}
fill={fill}
/>
</LabwareDetailGroup>
<g>
{flatMap(
Expand All @@ -78,6 +85,7 @@ export function StaticLabwareComponent(props: StaticLabwareProps): JSX.Element {
{...(isTiprack
? STYLE_BY_WELL_CONTENTS.tipPresent
: STYLE_BY_WELL_CONTENTS.defaultWell)}
fill={fill}
/>

{isTiprack ? (
Expand Down
6 changes: 4 additions & 2 deletions components/src/hardware-sim/Labware/labwareInternals/Well.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ export function WellComponent(props: WellProps): JSX.Element {
wellName,
stroke = COLORS.black90,
strokeWidth = 1,
fill = COLORS.white,
fill,
onMouseEnterWell,
onMouseLeaveWell,
isInteractive = onMouseEnterWell != null || onMouseLeaveWell != null,
} = props
const { x, y } = well

const wellFill = fill ?? COLORS.white

const pointerEvents: React.CSSProperties['pointerEvents'] = isInteractive
? 'auto'
: 'none'
Expand All @@ -46,7 +48,7 @@ export function WellComponent(props: WellProps): JSX.Element {
onMouseLeaveWell != null
? (event: React.MouseEvent) => onMouseLeaveWell({ wellName, event })
: undefined,
style: { pointerEvents, stroke, strokeWidth, fill },
style: { pointerEvents, stroke, strokeWidth, fill: wellFill },
}

if (well.shape === 'circular') {
Expand Down

0 comments on commit b5bc2e7

Please sign in to comment.