Skip to content

Commit

Permalink
VV: Add VolumetricViewer to lib-subject-viewers (#6349)
Browse files Browse the repository at this point in the history
- Add VolumetricViewer to lib-subject-viewers
- Add "data-testid" to all DOM elements we want to test for existence
- Add PropTypes to all Components based on linter output
- Add shims for Canvas, WebGL, and requestAnimationFrame for Cube rendering and testing
- Restructure VolumetricViewer directory to have all components, css, data, helpers, models, and tests in respective directories
- Specs for AlgorithmAStar
- Specs for ModelAnnotation
- Specs for ModelTool
- Specs for ModelViewer
- Specs for pointColor
- Specs for SortedSet
- Specs for VolumetricViewer that simply tests for existence in the DOM
- Write skeleton of a README
- Remove ProtoViewer. Clean up README. Fix Orbitals.
- Fix OrbitControls in the test:ci for GH
- Update yarn.lock
- Add instructions for M1 chips to lib-subject-viewers (#6385)

---------

Co-authored-by: Delilah C. <[email protected]>
  • Loading branch information
kieftrav and goplayoutside3 authored Oct 14, 2024
1 parent 4062f39 commit c0c1959
Show file tree
Hide file tree
Showing 34 changed files with 2,924 additions and 52 deletions.
8 changes: 6 additions & 2 deletions packages/lib-subject-viewers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ npm i @zooniverse/subject-viewers
and use it

```
import { ProtoViewer } from '@zooniverse/subject-viewers';
import { VolumetricViewer } from '@zooniverse/subject-viewers';
```

## Run
Expand All @@ -28,7 +28,7 @@ import { ProtoViewer } from '@zooniverse/subject-viewers';

Components should be added to the `src/components` folder and an export to `src/index.js`. Each component should be tested, documented readme, and have a storybook example added.

### Technologies and tools we use
## Technologies and tools we use

All of our components are written using React, built on top of Grommet, a component UI library, and styled by our custom Grommet style theme (@zooniverse/grommet-theme) and styled-components.

Expand All @@ -40,3 +40,7 @@ Testing is done by
- [Mocha](https://mochajs.org/) - test runner
- [Chai](https://www.chaijs.com/) - BDD/TDD assertion library
- [Sinon](https://sinonjs.org) - test spies, mocks, and stubs

## Troubleshooting

The VolumetricViewer component uses `canvas`. If your Mac has a M1 or M2 chip, you'll likely need to do some manual `brew install` commands in order to bootstrap FEM: https://github.com/Automattic/node-canvas/issues/1511.
13 changes: 10 additions & 3 deletions packages/lib-subject-viewers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@
"storybook": "storybook dev -p 6008",
"build-storybook": "storybook build",
"test": "mocha --config ./test/.mocharc.json ./.storybook/specConfig.js \"./src/**/*.spec.js\"",
"test:ci": "mocha --config ./test/.mocharc.json ./.storybook/specConfig.js --reporter=min \"./src/**/*.spec.js\""
"test:ci": "mocha --config ./test/.mocharc.json ./.storybook/specConfig.js --reporter=min \"./src/**/*.spec.js\"",
"watch": "watch 'yarn build' ./src",
"watch:test": "watch 'yarn test' ./src"
},
"dependencies": {
"buffer": "^6.0.3",
"three": "^0.162.0"
},
"dependencies": {},
"peerDependencies": {
"@zooniverse/grommet-theme": "3.x.x",
"grommet": "2.x.x",
Expand All @@ -45,13 +50,15 @@
"@storybook/addon-a11y": "~7.6.11",
"@storybook/addon-essentials": "~7.6.11",
"@storybook/react": "~7.6.11",
"canvas": "^2.11.2",
"chai": "~4.5.0",
"chai-dom": "~1.12.0",
"dirty-chai": "~2.0.1",
"mocha": "~10.7.3",
"sinon": "~17.0.0",
"sinon-chai": "~3.7.0",
"storybook": "~7.6.11"
"storybook": "~7.6.11",
"watch": "^1.0.2"
},
"engines": {
"node": ">=20.5"
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# VolumetricViewer

This directory holds all the relevant code for rendering the VolumetricViewer. There are two primary exports:

- `VolumetricViewerComponent` - a React component for the VolumetricViewer
- `VolumetricViewerData` - a function that returns the data with instantiated models along with the React Component
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { object, string } from 'prop-types'
import { useEffect, useState } from 'react'
import { Buffer } from 'buffer'
import { ComponentViewer } from './components/ComponentViewer.js'
import { ModelViewer } from './models/ModelViewer.js'
import { ModelAnnotations } from './models/ModelAnnotations.js'
import { ModelTool } from './models/ModelTool.js'

export default function VolumetricViewerComponent ({
config = {},
subjectData = '',
subjectUrl = '',
models
}) {
const [data, setData] = useState(null)
if (!models) {
const [modelState] = useState({
annotations: ModelAnnotations(),
tool: ModelTool(),
viewer: ModelViewer()
})
models = modelState
}

// Figure out subject data
useEffect(() => {
if (subjectData !== '') {
setData(Buffer.from(subjectData, 'base64'))
} else if (subjectUrl !== '') {
fetch(subjectUrl)
.then((res) => res.json())
.then((data) => {
setData(Buffer.from(data, 'base64'))
})
} else {
console.log('No data to display')
}
}, [])

// Loading screen will always display if we have no subject data
if (!data || !models) return <div>Loading...</div>

return (
<ComponentViewer
config={config}
data={data}
models={models}
/>
)
}

export const VolumetricViewerData = ({ subjectData = '', subjectUrl = '' }) => {
return {
data: {
config: {},
subjectData,
subjectUrl,
models: {
annotations: ModelAnnotations(),
tool: ModelTool(),
viewer: ModelViewer()
}
},
component: VolumetricViewerComponent
}
}

VolumetricViewerComponent.propTypes = {
config: object,
subjectData: string,
subjectUrl: string,
models: object
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { array, number, object } from 'prop-types'

export const AnnotationView = ({ annotation, annotations, index }) => {
function annotationActive () {
annotations.actions.annotation.active({ index })
}

function annotationDelete (e) {
e.stopPropagation()
annotations.actions.annotation.remove({ index })
}

const color = annotations.config.activeAnnotation === index ? '#555' : '#222'

return (
<li
style={{ padding: '20px', backgroundColor: color }}
onClick={annotationActive}
>
<p>Label: {annotation.label}</p>
<p>Threshold: {annotation.threshold}</p>
<p>Points: {annotation.points.active.length}</p>
<p onClick={annotationDelete}>Delete Annotation</p>
</li>
)
}

AnnotationView.propTypes = {
annotation: object,
annotations: array,
index: number
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { object } from 'prop-types'
import { AlgorithmAStar } from './../helpers/AlgorithmAStar.js'
import { Cube } from './Cube.js'
import { Plane } from './Plane.js'
import { Box } from 'grommet'

export const ComponentViewer = ({
data,
models
}) => {
// Initialize Annotations
if (models.annotations) {
models.annotations.initialize({
algorithm: AlgorithmAStar,
data: [], // will come from Caesar if they exist
viewer: models.viewer
})
}

// Initialize Tool
if (models.tool) {
models.tool.initialize({
annotations: models.annotations
})
}

// Initialize Viewer
if (models.viewer) {
models.viewer.initialize({
annotations: models.annotations,
data,
tool: models.tool
})
}

return (
<Box direction='row' style={{ maxWidth: '800px', padding: '20px' }}>
<Box flex>
{models.viewer.dimensions.map((dimensionName, dimension) => {
return (
<Plane
annotations={models.annotations}
dimension={dimension}
key={`dimension-${dimensionName}`}
tool={models.tool}
viewer={models.viewer}
/>
)
})}
</Box>
<Box flex>
<Cube
annotations={models.annotations}
tool={models.tool}
viewer={models.viewer}
/>
</Box>
</Box>
)
}

ComponentViewer.propTypes = {
data: object,
models: object
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { object } from 'prop-types'
import { useEffect, useState } from 'react'
import { AnnotationView } from './AnnotationView.js'
import { InputRangeDual } from './InputRangeDual.js'

export const Config = ({
annotations,
viewer
}) => {
const [_annotations, setAnnotations] = useState(annotations.annotations)

function annotationsChange ({ annotations }) {
setAnnotations([...annotations])
}

// State Change Management through useEffect()
useEffect(() => {
// State Listeners to bypass React rerenders
annotations.on('active:annotation', annotationsChange)
annotations.on('add:annotation', annotationsChange)
annotations.on('update:annotation', annotationsChange)
annotations.on('remove:annotation', annotationsChange)

return () => {
annotations.off('active:annotation', annotationsChange)
annotations.off('add:annotation', annotationsChange)
annotations.off('update:annotation', annotationsChange)
annotations.off('remove:annotation', annotationsChange)
}
}, [])

function downloadPoints () {
const rows = annotations.annotations.map((annotation) => {
return [
annotation.label,
annotation.threshold,
annotation.points.active.join('|'),
annotation.points.all.data.join('|')
]
})

rows.unshift([
'annotation name',
'annotation threshold',
'control points',
'connected points'
])
const csvContent =
'data:text/csv;charset=utf-8,' + rows.map((r) => r.join(',')).join('\n')
const encodedUri = encodeURI(csvContent)
const link = document.createElement('a')
link.setAttribute('href', encodedUri)
link.setAttribute('download', 'brainsweeper.csv')
document.body.appendChild(link)
link.click()
}

function saveScreenshot () {
viewer.saveScreenshot()
}

return (
<>
<h3 style={{ paddingBottom: '10px' }}>Volumetric File</h3>
<br />

<h3>Brightness Range</h3>
<InputRangeDual
valueMax={255}
valueMin={0}
valueMaxCurrent={viewer.threshold.max}
valueMinCurrent={viewer.threshold.min}
onChange={(min, max) => {
viewer.setThreshold({ min, max })
}}
/>
<br />
<br />

<button onClick={downloadPoints} style={{ marginBottom: '20px' }}>
Download Active Points
</button>

<button onClick={saveScreenshot} style={{ marginBottom: '20px' }}>
Save Screenshot
</button>

<ul>
{_annotations.map((annotation, index) => {
return (
<AnnotationView
annotation={annotation}
annotations={annotations}
index={index}
key={`annotation-${index}`}
/>
)
})}
</ul>
</>
)
}

Config.propTypes = {
annotations: object,
viewer: object
}
Loading

0 comments on commit c0c1959

Please sign in to comment.