diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js
index 5128b8fbb4..31b8f7431e 100644
--- a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js
+++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.js
@@ -2,18 +2,17 @@ import asyncStates from '@zooniverse/async-states'
import PropTypes from 'prop-types'
import { useTranslation } from '@translations/i18n'
import { lazy, Suspense } from 'react'
-// import VolumetricViewer from '@zooniverse/subject-viewers/VolumetricViewer'
import { withStores } from '@helpers'
import getViewer from './helpers/getViewer'
const VolumetricViewer = lazy(() => import('@zooniverse/subject-viewers/VolumetricViewer'))
-const ProtoViewer = lazy(() => import('@zooniverse/subject-viewers/ProtoViewer'))
function storeMapper(classifierStore) {
const {
subjects: { active: subject, loadingState: subjectQueueState },
- subjectViewer: { onSubjectReady, onError, loadingState: subjectReadyState }
+ subjectViewer: { onSubjectReady, onError, loadingState: subjectReadyState },
+ projects: { active: project }
} = classifierStore
const drawingTasks = classifierStore?.workflowSteps.findTasksByType('drawing')
@@ -22,6 +21,7 @@ function storeMapper(classifierStore) {
return {
enableInteractionLayer,
+ isVolumetricViewer: project?.isVolumetricViewer ?? false,
onError,
onSubjectReady,
subject,
@@ -32,6 +32,7 @@ function storeMapper(classifierStore) {
function SubjectViewer({
enableInteractionLayer,
+ isVolumetricViewer,
onError,
onSubjectReady,
subject,
@@ -52,12 +53,9 @@ function SubjectViewer({
return null
}
case asyncStates.success: {
- let Viewer
- if (subject?.viewer === 'volumetric') {
- Viewer = ProtoViewer
- } else {
- Viewer = getViewer(subject?.viewer)
- }
+ const Viewer = (isVolumetricViewer)
+ ? VolumetricViewer
+ : getViewer(subject?.viewer)
if (Viewer) {
return (
diff --git a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js
index 7f0d271949..35ac7a0dac 100644
--- a/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js
+++ b/packages/lib-classifier/src/components/Classifier/components/SubjectViewer/SubjectViewer.spec.js
@@ -1,60 +1,67 @@
+import { render, screen } from '@testing-library/react'
import asyncStates from '@zooniverse/async-states'
-import { shallow } from 'enzyme'
-
-import { SubjectViewer } from './SubjectViewer'
-import SingleImageViewer from './components/SingleImageViewer'
-import JSONDataViewer from './components/JSONDataViewer'
+import { Factory } from 'rosie'
+import mockStore from '@test/mockStore'
+import { Provider } from 'mobx-react'
+import SubjectType from '@store/SubjectStore/SubjectType'
+import { default as SubjectViewerWithStore, SubjectViewer } from './SubjectViewer'
describe('Component > SubjectViewer', function () {
it('should render without crashing', function () {
- const wrapper = shallow()
- expect(wrapper).to.be.ok()
+ render()
+ expect(screen).to.be.ok()
})
it('should render nothing if the subject store is initialized', function () {
- const wrapper = shallow()
- expect(wrapper.type()).to.be.null()
+ const { container } = render()
+ expect(container.firstChild).to.be.null()
})
-
+
it('should render a loading indicator if the subject store is loading', function () {
- const wrapper = shallow()
- expect(wrapper.text()).to.equal('SubjectViewer.loading')
+ render()
+ expect(screen.getByText('SubjectViewer.loading')).to.exist()
})
it('should render nothing if the subject store errors', function () {
- const wrapper = shallow()
- expect(wrapper.type()).to.be.null()
+ const { container } = render()
+ expect(container.firstChild).to.be.null()
})
- it('should render a subject viewer if the subject store successfully loads', function () {
- const wrapper = shallow()
- expect(wrapper.find(SingleImageViewer)).to.have.lengthOf(1)
- })
+ it('should render a subject viewer if the subject store successfully loads', async function () {
+ const store = mockStore({
+ subject: SubjectType.create(Factory.build('subject', { id: '1234' }))
+ })
+
+ render(
+
+ )
- it('should pass along the viewer configuration', function () {
- const viewerConfiguration = {
- zoomConfiguration: {
- direction: 'both',
- minZoom: 1,
- maxZoom: 10,
- zoomInValue: 1.2,
- zoomOutValue: 0.8
- }
- }
+ expect(screen.getByLabelText('Subject 1234')).to.exist()
+ })
- const wrapper = shallow()
- expect(wrapper.find(JSONDataViewer).props().viewerConfiguration).to.deep.equal(viewerConfiguration)
+ it('should render the VolumetricViewer if isVolumetricViewer = true', async function () {
+ render()
+ expect(screen.getByText('Suspense boundary')).to.exist()
+ expect(await screen.findByTestId('subject-viewer-volumetric')).to.exist()
})
describe('when there is an null viewer because of invalid subject media', function () {
it('should render null', function () {
- const wrapper = shallow(
+ const { container } = render(
)
- expect(wrapper.html()).to.be.null()
+ expect(container.firstChild).to.be.null()
})
})
})
diff --git a/packages/lib-classifier/src/store/Project/Project.js b/packages/lib-classifier/src/store/Project/Project.js
index 7a009f03b3..6fc0a93e90 100644
--- a/packages/lib-classifier/src/store/Project/Project.js
+++ b/packages/lib-classifier/src/store/Project/Project.js
@@ -24,6 +24,10 @@ const Project = types
get display_name() {
return self.strings.get('display_name')
},
+
+ get isVolumetricViewer() {
+ return self.experimental_tools.includes('volumetricViewer')
+ },
}))
export default types.compose('ProjectResource', Resource, Project)
diff --git a/packages/lib-subject-viewers/package.json b/packages/lib-subject-viewers/package.json
index 73b3603344..40bae97ff9 100644
--- a/packages/lib-subject-viewers/package.json
+++ b/packages/lib-subject-viewers/package.json
@@ -40,6 +40,7 @@
"three": "^0.162.0"
},
"peerDependencies": {
+ "@zooniverse/async-states": "~0.0.1",
"@zooniverse/grommet-theme": "3.x.x",
"grommet": "2.x.x",
"grommet-icons": "4.x.x",
diff --git a/packages/lib-subject-viewers/src/ProtoViewer/ProtoViewer.js b/packages/lib-subject-viewers/src/ProtoViewer/ProtoViewer.js
deleted file mode 100644
index fafd6ac63d..0000000000
--- a/packages/lib-subject-viewers/src/ProtoViewer/ProtoViewer.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import asyncStates from '@zooniverse/async-states'
-import { useEffect } from 'react'
-
-const DEFAULT_HANDLER = () => {}
-
-const defaultSubject = {
- id: '',
- locations: []
-}
-
-const defaultTarget = {
- clientHeight: 0,
- clientWidth: 0,
- naturalHeight: 0,
- naturalWidth: 0
-}
-
-export default function ProtoViewer({
- /*
- loadingState is defined in SubjectViewerStore.js. onReady and onError affect it
- and then it's used below to choose rendering options
- */
- loadingState = asyncStates.initialized,
- /*
- onError is defined in lib-classifier's SubjectViewerStore.js. When called, it sets
- SubjectViewerStore's loadingState to error.
- */
- onError = DEFAULT_HANDLER,
- /*
- onReady is defined in lib-classifier's SubjectViewerStore.js as onSubjectReady().
- It sets the loadingState on the SubjectViewerStore to success. That success triggers
- an update of subjectReadyState in SubjectViewer.js which is passed as loadingState to
- the Viewer *and* the TaskArea, preventing users from making classifications when a subject
- viewer is not ready.
- */
- onReady = DEFAULT_HANDLER,
- subject = defaultSubject
-}) {
- /*
- Here's where unique handling of subject data happens every time the subject changes.
- Examples are useSubjectImage() or useSubjectJSON() hooks in lib-classifier.
- */
- useEffect(
- function onSubjectChange() {
- function handleSubject() {} // etc, fetch more subject data if needed
- onReady(defaultTarget)
- },
- [subject]
- )
-
- /*
- These are the render options. First catch an error if needed,
- then render subject viewer as long as the loadingState says it's okay.
- */
- if (loadingState === asyncStates.error) {
- return
Something went wrong.
- }
-
- if (loadingState !== asyncStates.initialized) {
- return (
- This is the ProtoViewer. Here's the subject ID: {subject.id}
- )
- }
-
- return null
-}
diff --git a/packages/lib-subject-viewers/src/ProtoViewer/index.js b/packages/lib-subject-viewers/src/ProtoViewer/index.js
deleted file mode 100644
index 13a9d8cff1..0000000000
--- a/packages/lib-subject-viewers/src/ProtoViewer/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './ProtoViewer.js'
diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js b/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js
index 71b604911c..fd7f3d27e0 100644
--- a/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js
+++ b/packages/lib-subject-viewers/src/VolumetricViewer/VolumetricViewer.js
@@ -1,52 +1,49 @@
-import { object, string } from 'prop-types'
-import { useEffect, useState } from 'react'
-import { Buffer } from 'buffer'
+import { func, object, string } from 'prop-types'
+import { useState } from 'react'
import { ComponentViewer } from './components/ComponentViewer.js'
import { ModelViewer } from './models/ModelViewer.js'
import { ModelAnnotations } from './models/ModelAnnotations.js'
import { ModelTool } from './models/ModelTool.js'
+import { useVolumetricSubject } from './../hooks/useVolumetricSubject.js'
+import asyncStates from '@zooniverse/async-states'
+
+const DEFAULT_HANDLER = () => {}
export default function VolumetricViewer ({
- config = {},
- subjectData = '',
- subjectUrl = '',
- models
+ loadingState = asyncStates.initialized,
+ onError = DEFAULT_HANDLER,
+ onReady = DEFAULT_HANDLER,
+ subject
}) {
- 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')
- }
- }, [])
+ const { data, loading, error } = useVolumetricSubject({ onError, onReady, subject })
- // Loading screen will always display if we have no subject data
- if (!data || !models) return Loading...
+ const [modelState] = useState({
+ annotations: ModelAnnotations(),
+ tool: ModelTool(),
+ viewer: ModelViewer()
+ })
+
+ const isLoading = loadingState === asyncStates.initialized
+ || loadingState === asyncStates.loading
+ || loading;
+ const isError = loadingState === asyncStates.error
+ || error
+ || data === null;
- return (
-
- )
+ // Specs should skip rendering the VolumetricViewer component
+ // WebGL/Canvas throws exceptions when running specs due to non-browser environment
+ return (data === 'mock-subject-json')
+ ?
+ : (isLoading)
+ ? Loading...
+ : (isError)
+ ? Error
+ :
}
export const VolumetricViewerData = ({ subjectData = '', subjectUrl = '' }) => {
@@ -66,8 +63,8 @@ export const VolumetricViewerData = ({ subjectData = '', subjectUrl = '' }) => {
}
VolumetricViewer.propTypes = {
- config: object,
- subjectData: string,
- subjectUrl: string,
- models: object
+ loadingState: string,
+ onError: func,
+ onReady: func,
+ subject: object
}
diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/components/ComponentViewer.js b/packages/lib-subject-viewers/src/VolumetricViewer/components/ComponentViewer.js
index ba13925811..22eeb477ce 100644
--- a/packages/lib-subject-viewers/src/VolumetricViewer/components/ComponentViewer.js
+++ b/packages/lib-subject-viewers/src/VolumetricViewer/components/ComponentViewer.js
@@ -1,4 +1,4 @@
-import { object } from 'prop-types'
+import { object, string } from 'prop-types'
import { AlgorithmAStar } from './../helpers/AlgorithmAStar.js'
import { Cube } from './Cube.js'
import { Plane } from './Plane.js'
@@ -60,6 +60,6 @@ export const ComponentViewer = ({
}
ComponentViewer.propTypes = {
- data: object,
+ data: string,
models: object
}
diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/components/Cube.js b/packages/lib-subject-viewers/src/VolumetricViewer/components/Cube.js
index 63ed209ae0..d84f8ca95f 100644
--- a/packages/lib-subject-viewers/src/VolumetricViewer/components/Cube.js
+++ b/packages/lib-subject-viewers/src/VolumetricViewer/components/Cube.js
@@ -24,13 +24,13 @@ import { pointColor } from './../helpers/pointColor.js'
import { SortedSetUnion } from './../helpers/SortedSet.js'
// Shim for test:ci in GH needs this to work
-let OrbitControls = null;
+const OrbitControls = null
// import("three/addons/controls/OrbitControls.js").then((module) => {
-// OrbitControls = module.OrbitControls;
+// OrbitControls = module.OrbitControls;
// })
// Shim for node.js testing
-let glContext = null
+const glContext = null
if (!process.browser) {
window.requestAnimationFrame = () => {
// needs to be stubbed out for animate() to work
@@ -148,17 +148,17 @@ export const Cube = ({ annotations, tool, viewer }) => {
threeRef.current.renderer.setPixelRatio(window.devicePixelRatio)
threeRef.current.renderer.setSize(width, width)
- if (OrbitControls) {
- threeRef.current.orbit = new OrbitControls(
- threeRef.current.camera,
- threeRef.current.renderer.domElement,
- );
- threeRef.current.orbit.enableDamping = false;
- threeRef.current.orbit.enableZoom = true;
- threeRef.current.orbit.enablePan = false;
- } else {
- console.log('OrbitControls are not available')
- }
+ if (OrbitControls) {
+ threeRef.current.orbit = new OrbitControls(
+ threeRef.current.camera,
+ threeRef.current.renderer.domElement
+ )
+ threeRef.current.orbit.enableDamping = false
+ threeRef.current.orbit.enableZoom = true
+ threeRef.current.orbit.enablePan = false
+ } else {
+ console.log('OrbitControls are not available')
+ }
// View Axes
const half = viewer.base / 2
diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/data/subjectMock.js b/packages/lib-subject-viewers/src/VolumetricViewer/data/subjectMock.js
new file mode 100644
index 0000000000..96d3c460bf
--- /dev/null
+++ b/packages/lib-subject-viewers/src/VolumetricViewer/data/subjectMock.js
@@ -0,0 +1,9 @@
+export const VolumetricSubjectMock = {
+ "id": "mock_subject",
+ "locations": [
+ {
+ "application/json": "https://panoptes-uploads.zooniverse.org/subject_location/34898ede-7e3d-4f83-b6ee-920769f01288.json"
+ }
+ ],
+ "subjectJSON": "GRnI+hnIr5bIr5Z9r5Z9+uHIr5bIr5Z9r5Z9ZJZ9ZEv6r5Z9r5Z9ZJZ9ZEt9ZEsyGZZ9GRl9ZEt9ZEsyGUsyGQ=="
+}
diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelViewer.js b/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelViewer.js
index 9dea4de750..7453f6f17c 100644
--- a/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelViewer.js
+++ b/packages/lib-subject-viewers/src/VolumetricViewer/models/ModelViewer.js
@@ -1,3 +1,4 @@
+import { Buffer } from 'buffer'
import { SortedSet } from './../helpers/SortedSet.js'
export const ModelViewer = () => {
@@ -16,8 +17,8 @@ export const ModelViewer = () => {
threshold: { min: 0, max: 255 },
// initialize
initialize: ({ data }) => {
- pointModel.data = data
- pointModel.base = Math.cbrt(data.length)
+ pointModel.data = Buffer.from(data, 'base64')
+ pointModel.base = Math.cbrt(pointModel.data.length)
pointModel.baseFrameMod = [
Math.pow(pointModel.base, 2),
pointModel.base,
diff --git a/packages/lib-subject-viewers/src/VolumetricViewer/tests/VolumetricViewer.stories.js b/packages/lib-subject-viewers/src/VolumetricViewer/tests/VolumetricViewer.stories.js
index 406b12b0da..362a74b8dc 100644
--- a/packages/lib-subject-viewers/src/VolumetricViewer/tests/VolumetricViewer.stories.js
+++ b/packages/lib-subject-viewers/src/VolumetricViewer/tests/VolumetricViewer.stories.js
@@ -1,5 +1,6 @@
import VolumetricViewer from './../VolumetricViewer'
-import subjectData from './../data/4x4x4.json'
+import asyncStates from '@zooniverse/async-states'
+import { VolumetricSubjectMock } from './../data/subjectMock'
export default {
title: 'Components / VolumetricViewer',
@@ -7,5 +8,10 @@ export default {
}
export const Default = () => {
- return
+ return (
+
+ )
}
diff --git a/packages/lib-subject-viewers/src/hooks/useVolumetricSubject.js b/packages/lib-subject-viewers/src/hooks/useVolumetricSubject.js
new file mode 100644
index 0000000000..392196397e
--- /dev/null
+++ b/packages/lib-subject-viewers/src/hooks/useVolumetricSubject.js
@@ -0,0 +1,40 @@
+// Inspired by useSubjectJSON.js in the lib-classifier package
+import { useEffect, useState } from 'react';
+
+export const useVolumetricSubject = ({ onError, onReady, subject }) => {
+ const [error, setError] = useState()
+ const [data, setData] = useState()
+
+ useEffect(() => {
+ setData(null)
+ setError(null)
+
+ if (!subject) return setError('No subject found')
+ // subjectJSON is used for testing to avoid network requests
+ if (subject?.subjectJSON) return setData(subject.subjectJSON)
+
+ const jsonLocation =
+ subject.locations.find(
+ (l) => l.type === 'application' || l.type === 'text'
+ ) || {};
+
+ if (!jsonLocation.url) return setError('No JSON url found for this subject')
+
+ fetch(jsonLocation.url)
+ .then((res) => res.json())
+ .then((data) => {
+ setData(data)
+ onReady();
+ })
+ .catch((err) => {
+ console.log('useVolumetricSubject() error', err)
+ onError(err)
+ });
+ }, [subject]);
+
+ return {
+ data: data,
+ loading: !data && !error,
+ error
+ };
+};
diff --git a/packages/lib-subject-viewers/src/index.js b/packages/lib-subject-viewers/src/index.js
index 3dfe5d0b9f..6685d85861 100644
--- a/packages/lib-subject-viewers/src/index.js
+++ b/packages/lib-subject-viewers/src/index.js
@@ -1,4 +1,3 @@
export { default as VolumetricViewer } from './VolumetricViewer/VolumetricViewer.js'
-export { default as ProtoViewer } from './ProtoViewer'
export { VolumetricViewerData } from './VolumetricViewer/VolumetricViewer.js'