Skip to content

Commit 71f5f4a

Browse files
authored
Merge pull request #214 from MetaCell/feature/VFB-229
#vfb-229 - POC: add Neuroglass viewer component and associated state management; integrate into toolbar and layout.
2 parents 757017e + da5d8f6 commit 71f5f4a

6 files changed

Lines changed: 277 additions & 2 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { useState, useEffect, useMemo } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import { Box, Typography } from '@mui/material';
4+
import { getNeuroglassState, hasNeuroglassState } from '../utils/neuroglassStateConfig';
5+
6+
const NEUROGLASS_URL = import.meta.env.VITE_NEUROGLASS_URL || 'https://www.research.neuroglass.dev.metacell.us';
7+
8+
export default function NeuroglassViewer() {
9+
const [iframeSrc, setIframeSrc] = useState('');
10+
11+
const focusedInstance = useSelector(state => state.instances?.focusedInstance);
12+
13+
const iframeSrcUrl = useMemo(() => {
14+
if (!focusedInstance?.metadata?.Id) return '';
15+
16+
// Priority 1: Predefined state for focused instance (if available)
17+
let stateToUse = null;
18+
if (hasNeuroglassState(focusedInstance.metadata.Id)) {
19+
stateToUse = getNeuroglassState(focusedInstance.metadata.Id);
20+
}
21+
// Priority 2: Default to template
22+
else {
23+
stateToUse = getNeuroglassState('VFB_00101567');
24+
}
25+
26+
if (!stateToUse) return '';
27+
28+
const stateStr = JSON.stringify(stateToUse);
29+
const encodedState = encodeURIComponent(stateStr);
30+
return `${NEUROGLASS_URL}/new#!${encodedState}`;
31+
}, [focusedInstance?.metadata?.Id]);
32+
33+
useEffect(() => {
34+
if (iframeSrcUrl) {
35+
setIframeSrc(iframeSrcUrl);
36+
}
37+
}, [iframeSrcUrl]);
38+
39+
return (
40+
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
41+
{iframeSrc ? (
42+
<Box sx={{ flex: 1, border: '1px solid #ccc', borderRadius: 1, overflow: 'hidden' }}>
43+
<iframe
44+
src={iframeSrc}
45+
style={{
46+
width: '100%',
47+
height: '100%',
48+
border: 'none',
49+
backgroundColor: '#000',
50+
}}
51+
title="Neuroglass Viewer"
52+
allow="accelerometer; camera; gyroscope; microphone; web-share"
53+
sandbox="allow-scripts allow-same-origin allow-forms"
54+
/>
55+
</Box>
56+
) : (
57+
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fafafa' }}>
58+
<Typography color="textSecondary">Loading Neuroglass viewer...</Typography>
59+
</Box>
60+
)}
61+
</Box>
62+
);
63+
}

applications/virtual-fly-brain/frontend/src/components/configuration/VFBToolbar/vfbtoolbarMenuConfiguration.jsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ export const toolbarMenu = (autoSaveLayout) => { return {
242242
parameters: [widgets?.stackViewerWidget?.id]
243243
}
244244
},
245+
{
246+
label: "Neuroglass Viewer",
247+
icon: "fa fa-brain",
248+
action: {
249+
handlerAction: ACTIONS.SHOW_WIDGET,
250+
parameters: [widgets?.neuroglassViewerWidget?.id]
251+
}
252+
},
245253
{
246254
label: "Template ROI Browser",
247255
icon: "fa fa-indent",

applications/virtual-fly-brain/frontend/src/components/layout/componentMap.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ThreeDCanvas from '../ThreeDCanvas';
44
import VFBCircuitBrowser from '../VFBCircuitBrowser';
55
import VFBGraph from '../VFBGraph';
66
import VFBListViewer from '../VFBListViewer';
7+
import NeuroglassViewer from '../NeuroglassViewer';
78
/**
89
* Key of the component is the `component` attribute of the widgetConfiguration.
910
* This map is used inside the LayoutManager to know which component to display for a given widget.
@@ -14,7 +15,8 @@ const componentMap = {
1415
'roiBrowser': ROIBrowser,
1516
'termContext' : VFBGraph,
1617
'circuitBrowser' : VFBCircuitBrowser,
17-
'listViewer': VFBListViewer
18+
'listViewer': VFBListViewer,
19+
'neuroglassViewer': NeuroglassViewer
1820
};
1921

2022
export default componentMap

applications/virtual-fly-brain/frontend/src/components/layout/widgets.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export const widgetsIDs = {
66
roiBrowserWidgetID : 'roiBrowserWidget',
77
termContextWidgetID : 'termContextWidget',
88
circuitBrowserWidgetID : 'circuitBrowserWidget',
9-
listViewerWidgetID : 'listViewerWidget'
9+
listViewerWidgetID : 'listViewerWidget',
10+
neuroglassViewerWidgetID : 'neuroglassViewerWidget'
1011
};
1112

1213
export const widgets = {
@@ -81,4 +82,16 @@ export const widgets = {
8182
pos: 4,
8283
props: { size: { height: 600, width: 300 } }
8384
},
85+
86+
neuroglassViewerWidget : {
87+
id: widgetsIDs.neuroglassViewerWidgetID,
88+
name: "Neuroglass Viewer",
89+
component: "neuroglassViewer",
90+
panelName: "right",
91+
hideOnClose: true,
92+
status: WidgetStatus.HIDDEN,
93+
defaultPosition: 'RIGHT',
94+
pos: 5,
95+
props: { size: { height: 600, width: 800 } }
96+
},
8497
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Neuroglass Widget Actions
3+
* Controls visibility and state of the Neuroglass viewer widget
4+
*/
5+
6+
import { setWidgetVisible } from '@metacell/geppetto-meta-client/common/layout/actions';
7+
import { WidgetStatus } from '@metacell/geppetto-meta-client/common/layout/model';
8+
import { widgetsIDs } from '../components/layout/widgets';
9+
import { hasNeuroglassState, getNeuroglassState, NEUROGLASS_STATES_MAP } from './neuroglassStateConfig';
10+
11+
12+
/**
13+
* Show the Neuroglass viewer widget
14+
* @param {Object} store - Redux store instance
15+
*/
16+
export const showNeuroglassViewer = (store) => {
17+
store.dispatch(setWidgetVisible(widgetsIDs.neuroglassViewerWidgetID, true));
18+
};
19+
20+
/**
21+
* Hide the Neuroglass viewer widget
22+
* @param {Object} store - Redux store instance
23+
*/
24+
export const hideNeuroglassViewer = (store) => {
25+
store.dispatch(setWidgetVisible(widgetsIDs.neuroglassViewerWidgetID, false));
26+
};
27+
28+
/**
29+
* Toggle Neuroglass viewer widget visibility
30+
* @param {Object} store - Redux store instance
31+
*/
32+
export const toggleNeuroglassViewer = (store) => {
33+
const state = store.getState();
34+
const widgets = state.widgets || {};
35+
const neuroglassWidget = widgets[widgetsIDs.neuroglassViewerWidgetID];
36+
37+
const isVisible = neuroglassWidget?.status === WidgetStatus.ACTIVE;
38+
store.dispatch(setWidgetVisible(widgetsIDs.neuroglassViewerWidgetID, !isVisible));
39+
};
40+
41+
/**
42+
* Check if an instance has Neuroglass data available
43+
* @param {string} instanceId - VFB instance ID
44+
* @returns {boolean} True if Neuroglass data exists
45+
*/
46+
export const hasNeuroglassData = (instanceId) => {
47+
return hasNeuroglassState(instanceId);
48+
};
49+
50+
/**
51+
* Get the Neuroglass viewer state for a specific instance
52+
* @param {string} instanceId - VFB instance ID
53+
* @returns {Object|null} Neuroglass state or null if not available
54+
*/
55+
export const getNeuroglassStateForInstance = (instanceId) => {
56+
return getNeuroglassState(instanceId);
57+
};
58+
59+
/**
60+
* Auto-show Neuroglass widget when a compatible instance is loaded
61+
* @param {Object} store - Redux store instance
62+
*/
63+
export const autoShowNeuroglass = (store) => {
64+
const state = store.getState();
65+
const loadedInstances = state.instances.allLoadedInstances || [];
66+
67+
const hasCompatibleInstance = loadedInstances.some(
68+
instance => hasNeuroglassData(instance.metadata?.Id)
69+
);
70+
71+
if (hasCompatibleInstance) {
72+
showNeuroglassViewer(store);
73+
}
74+
};
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
const SHARED_VIEWPORT = {
2+
dimensions: {
3+
x: [1e-9, 'm'],
4+
y: [5.189161e-10, 'm'],
5+
z: [5.189161e-10, 'm'],
6+
},
7+
relativeDisplayScales: { x: 2, y: 2, z: 2 },
8+
position: [87.5, 280.5, 605.5],
9+
crossSectionScale: 0.5,
10+
projectionOrientation: [
11+
0.03356223925948143,
12+
-0.7611185908317566,
13+
-0.049303606152534485,
14+
-0.645864725112915,
15+
],
16+
projectionScale: 1024,
17+
};
18+
19+
const SHARED_LAYERS = [
20+
{
21+
type: 'image',
22+
source: 'gs://neuroglass/vfb/VFB_00101567_1567/|neuroglancer-precomputed:',
23+
tab: 'rendering',
24+
shader: '#uicontrol invlerp normalized\nvoid main() {\n float val = toNormalized(getDataValue());\n emitRGBA(vec4(val, val, val, val * 0.3)); // Grayscale with 30% opacity\n}\n\n',
25+
volumeRendering: 'on',
26+
renderingAccordion: { volumeRenderingExpanded: true },
27+
name: 'VFB_00101567_1567',
28+
},
29+
{
30+
type: 'image',
31+
source: 'gs://neuroglass/vfb/VFB_00101567_12vj/|neuroglancer-precomputed:',
32+
tab: 'rendering',
33+
shader: '#uicontrol invlerp normalized\nvoid main() {\n float val = toNormalized(getDataValue());\n emitRGB(vec3(0.0, val, 0.0)); // Green\n}',
34+
volumeRendering: 'on',
35+
volumeRenderingDepthSamples: 90.50966799187809,
36+
renderingAccordion: { volumeRenderingExpanded: true },
37+
name: 'VFB_00101567_12vj',
38+
},
39+
{
40+
type: 'image',
41+
source: 'gs://neuroglass/vfb/VFB_00101567/|neuroglancer-precomputed:',
42+
tab: 'source',
43+
shader: '#uicontrol invlerp normalized\nvoid main() {\n float val = toNormalized(getDataValue());\n emitRGB(vec3(val, 0.0, val)); // Magenta\n}\n',
44+
volumeRendering: 'on',
45+
volumeRenderingDepthSamples: 90.50966799187809,
46+
renderingAccordion: { volumeRenderingExpanded: true },
47+
name: 'VFB_00101567_101b',
48+
},
49+
];
50+
51+
const createNeuroglassState = (selectedLayerId) => ({
52+
...SHARED_VIEWPORT,
53+
layers: SHARED_LAYERS,
54+
showSlices: false,
55+
selectedLayer: { visible: false, layer: selectedLayerId },
56+
layout: '4panel-alt',
57+
layerListPanel: { visible: false },
58+
});
59+
60+
export const NEUROGLASS_STATE_VFB_00101567 = createNeuroglassState('VFB_00101567_1567');
61+
62+
export const NEUROGLASS_STATE_VFB_0010101b = createNeuroglassState('VFB_00101567_101b');
63+
64+
export const NEUROGLASS_STATE_VFB_001012vj = createNeuroglassState('VFB_00101567_12vj');
65+
66+
export const NEUROGLASS_STATES_MAP = {
67+
'VFB_00101567': NEUROGLASS_STATE_VFB_00101567,
68+
'VFB_0010101b': NEUROGLASS_STATE_VFB_0010101b,
69+
'VFB_001012vj': NEUROGLASS_STATE_VFB_001012vj,
70+
};
71+
72+
export const SUPPORTED_NEUROGLASS_INSTANCES = Object.freeze([
73+
'VFB_00101567',
74+
'VFB_0010101b',
75+
'VFB_001012vj',
76+
]);
77+
78+
const validateInstanceId = (instanceId) => {
79+
if (typeof instanceId !== 'string') {
80+
return { valid: false, error: `Expected string, got ${typeof instanceId}` };
81+
}
82+
if (!/^VFB_[A-Za-z0-9]+$/.test(instanceId)) {
83+
return { valid: false, error: `Invalid VFB ID format: ${instanceId}` };
84+
}
85+
return { valid: true };
86+
};
87+
88+
export const getNeuroglassState = (instanceId) => {
89+
const validation = validateInstanceId(instanceId);
90+
if (!validation.valid) {
91+
console.error(`[Neuroglass] ${validation.error}`);
92+
return null;
93+
}
94+
const state = NEUROGLASS_STATES_MAP[instanceId];
95+
if (!state) console.warn(`[Neuroglass] No state for instance: ${instanceId}`);
96+
return state || null;
97+
};
98+
99+
export const hasNeuroglassState = (instanceId) => {
100+
const validation = validateInstanceId(instanceId);
101+
if (!validation.valid) return false;
102+
return Object.prototype.hasOwnProperty.call(NEUROGLASS_STATES_MAP, instanceId);
103+
};
104+
105+
export const isNeuroglassSupportedInstance = (instanceId) => {
106+
const validation = validateInstanceId(instanceId);
107+
if (!validation.valid) return false;
108+
return SUPPORTED_NEUROGLASS_INSTANCES.includes(instanceId);
109+
};
110+
111+
export const NEUROGLASS_CONFIG = {
112+
instances: SUPPORTED_NEUROGLASS_INSTANCES,
113+
layers: SHARED_LAYERS,
114+
viewport: SHARED_VIEWPORT,
115+
};

0 commit comments

Comments
 (0)