Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Box, Typography } from '@mui/material';
import { getNeuroglasserState, hasNeuroglasserState } from '../utils/neuroglassStateConfig';

const NEUROGLASS_URL = import.meta.env.VITE_NEUROGLASS_URL || 'https://www.research.neuroglass.dev.metacell.us';

export default function NeuroglassViewer() {
const [iframeSrc, setIframeSrc] = useState('');

const focusedInstance = useSelector(state => state.instances?.focusedInstance);

const iframeSrcUrl = useMemo(() => {
if (!focusedInstance?.metadata?.Id) return '';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use template fallback when focused instance is absent

The early return on missing focus ID (return '') prevents the component from ever reaching the default-template fallback branch, so opening this widget before an instance is focused leaves iframeSrc empty and the panel stuck on the loading placeholder. That breaks the intended “focused instance or default template” behavior in startup/empty-focus states.

Useful? React with 👍 / 👎.


// Priority 1: Predefined state for focused instance (if available)
let stateToUse = null;
if (hasNeuroglasserState(focusedInstance.metadata.Id)) {
stateToUse = getNeuroglasserState(focusedInstance.metadata.Id);
}
// Priority 2: Default to template
else {
stateToUse = getNeuroglasserState('VFB_00101567');
}

if (!stateToUse) return '';

const stateStr = JSON.stringify(stateToUse);
return `${NEUROGLASS_URL}/new#!${stateStr}`;
}, [focusedInstance?.metadata?.Id]);

useEffect(() => {
if (iframeSrcUrl) {
setIframeSrc(iframeSrcUrl);
}
}, [iframeSrcUrl]);

return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{iframeSrc ? (
<Box sx={{ flex: 1, border: '1px solid #ccc', borderRadius: 1, overflow: 'hidden' }}>
<iframe
src={iframeSrc}
style={{
width: '100%',
height: '100%',
border: 'none',
backgroundColor: '#000',
}}
title="Neuroglass Viewer"
sandbox="allow-scripts allow-popups allow-forms"
/>
</Box>
) : (
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fafafa' }}>
<Typography color="textSecondary">Loading Neuroglass viewer...</Typography>
</Box>
)}
</Box>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ export const toolbarMenu = (autoSaveLayout) => { return {
parameters: [widgets?.stackViewerWidget?.id]
}
},
{
label: "Neuroglass Viewer",
icon: "fa fa-brain",
action: {
handlerAction: ACTIONS.SHOW_WIDGET,
parameters: [widgets?.neuroglassViewerWidget?.id]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle missing Neuroglass widget in imported layouts

This menu item assumes state.widgets['neuroglassViewerWidget'] exists, but menuHandler builds the widget from Redux (shared/header/index.jsx, widgets[action.parameters[0]]) and imported custom layouts overwrite widgets from saved data (reducers/middleware/vfbMiddleware.js, widgets: { ...action.data.redux.widgets }). If a user loads a layout saved before this commit, that key is absent, so clicking “Neuroglass Viewer” dispatches an incomplete widget update and the viewer does not open.

Useful? React with 👍 / 👎.

}
},
{
label: "Template ROI Browser",
icon: "fa fa-indent",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ThreeDCanvas from '../ThreeDCanvas';
import VFBCircuitBrowser from '../VFBCircuitBrowser';
import VFBGraph from '../VFBGraph';
import VFBListViewer from '../VFBListViewer';
import NeuroglassViewer from '../NeuroglassViewer';
/**
* Key of the component is the `component` attribute of the widgetConfiguration.
* This map is used inside the LayoutManager to know which component to display for a given widget.
Expand All @@ -14,7 +15,8 @@ const componentMap = {
'roiBrowser': ROIBrowser,
'termContext' : VFBGraph,
'circuitBrowser' : VFBCircuitBrowser,
'listViewer': VFBListViewer
'listViewer': VFBListViewer,
'neuroglassViewer': NeuroglassViewer
};

export default componentMap
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export const widgetsIDs = {
roiBrowserWidgetID : 'roiBrowserWidget',
termContextWidgetID : 'termContextWidget',
circuitBrowserWidgetID : 'circuitBrowserWidget',
listViewerWidgetID : 'listViewerWidget'
listViewerWidgetID : 'listViewerWidget',
neuroglassViewerWidgetID : 'neuroglassViewerWidget'
};

export const widgets = {
Expand Down Expand Up @@ -81,4 +82,16 @@ export const widgets = {
pos: 4,
props: { size: { height: 600, width: 300 } }
},

neuroglassViewerWidget : {
id: widgetsIDs.neuroglassViewerWidgetID,
name: "Neuroglass Viewer",
component: "neuroglassViewer",
panelName: "right",
hideOnClose: true,
status: WidgetStatus.HIDDEN,
defaultPosition: 'RIGHT',
pos: 5,
props: { size: { height: 600, width: 800 } }
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Neuroglancer State Mapper
*
* Converts between VFB's Redux state and Neuroglancer state schema
* compatible with Neuroglass storage format.
*/

/**
* Convert VFB Redux state to Neuroglancer state schema
* @param {Object} vfbState - Full Redux state from VFB
* @returns {Object} Neuroglancer state object
*/
export const vfbToNeuroglancerState = (vfbState) => {
const { instances, globalInfo } = vfbState;

Check failure on line 14 in applications/virtual-fly-brain/frontend/src/utils/neuroglancerStateMapper.js

View workflow job for this annotation

GitHub Actions / eslint

'globalInfo' is assigned a value but never used. Allowed unused vars must match /^[A-Z_]|^_/u
const { allLoadedInstances, launchTemplate, focusedInstance } = instances;

// Convert VFB instances to Neuroglancer layers
const layers = [];

// Add template as base layer if it exists
if (launchTemplate) {
layers.push({
type: 'segmentation',
source: `precomputed://https://neuroglass.metacell.us/datasets/${launchTemplate.metadata?.Id}`,
tab: 'source',
name: launchTemplate.metadata?.Label || 'Template',
visible: true,
opacity: 0.5,
});
}

// Add all loaded instances as layers
allLoadedInstances?.forEach((instance) => {
const isFocused = focusedInstance?.metadata?.Id === instance.metadata?.Id;

layers.push({
type: 'segmentation',
source: `precomputed://https://neuroglass.metacell.us/datasets/${instance.metadata?.Id}`,
tab: 'segments',
name: instance.metadata?.Label || instance.metadata?.Id,
visible: true,
segments: [instance.metadata?.Id],
segmentColors: {
[instance.metadata?.Id]: instance.color || '#ffffff',
},
// Highlight focused instance
...(isFocused && {
selectedSegment: instance.metadata?.Id,
}),
});
});

// Build navigation/camera state
const navigation = {

Check failure on line 54 in applications/virtual-fly-brain/frontend/src/utils/neuroglancerStateMapper.js

View workflow job for this annotation

GitHub Actions / eslint

'navigation' is assigned a value but never used. Allowed unused vars must match /^[A-Z_]|^_/u
pose: {
position: {
voxelSize: [1.0, 0.5189161, 0.5189161], // VFB default from precomputed data
voxelCoordinates: [87, 283, 605], // Center of typical fly brain
},
},
zoomFactor: 8,
};

// Build complete Neuroglancer state
return {
dimensions: {
x: [1e-9, 'm'],
y: [1e-9, 'm'],
z: [1e-9, 'm'],
},
position: [87000, 283000, 605000],
crossSectionOrientation: [0, 0, 0, 1],
crossSectionScale: 1,
projectionOrientation: [0, 0, 0, 1],
projectionScale: 16384,
layers,
selectedLayer: {
visible: true,
layer: focusedInstance?.metadata?.Label || layers[0]?.name,
},
layout: '4panel',
};
};

/**
* Convert Neuroglancer state to VFB-compatible format
* Note: This is a partial conversion for loading studies back into VFB
* @param {Object} ngState - Neuroglancer state object
* @returns {Object} VFB-compatible state updates
*/
export const neuroglancerToVFBState = (ngState) => {
const { layers, selectedLayer } = ngState;

// Extract instance IDs from Neuroglancer layers
const instanceIds = layers
?.filter((layer) => layer.type === 'segmentation')
?.map((layer) => {
// Try to extract VFB ID from source URL
const match = layer.source?.match(/\/datasets\/([A-Z]+_\d+)/);
return match ? match[1] : null;
})
.filter(Boolean);

// Determine focused instance from selectedLayer
const focusedLayerName = selectedLayer?.layer;
const focusedLayer = layers?.find((layer) => layer.name === focusedLayerName);
const focusedId = focusedLayer?.source?.match(/\/datasets\/([A-Z]+_\d+)/)?.[1];

return {
instanceIds,
focusedId,
// Camera/navigation state could be extracted from ngState.position, etc.
};
};

/**
* Create a minimal Neuroglancer state for a single VFB instance
* Useful for quick sharing or bookmarking
* @param {Object} instance - VFB instance object
* @returns {Object} Minimal Neuroglancer state
*/
export const instanceToNeuroglancerState = (instance) => {
return {
layers: [
{
type: 'segmentation',
source: `precomputed://https://neuroglass.metacell.us/datasets/${instance.metadata?.Id}`,
name: instance.metadata?.Label || instance.metadata?.Id,
segments: [instance.metadata?.Id],
},
],
layout: 'xy',
};
};

export default {
vfbToNeuroglancerState,
neuroglancerToVFBState,
instanceToNeuroglancerState,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Neuroglass Widget Actions
* Controls visibility and state of the Neuroglass viewer widget
*/

import { setWidgetVisible } from '@metacell/geppetto-meta-client/common/layout/actions';
import { widgetsIDs } from '../layout/widgets';
import { hasNeuroglasserState, getNeuroglasserState } from './neuroglassStateConfig';

// Instances with available Neuroglass datasets
export const NEUROGLASS_COMPATIBLE_INSTANCES = [
'VFB_0010101b',
'VFB_001012vj',
'VFB_00101567',
];

/**
* Show the Neuroglass viewer widget
* @param {Object} store - Redux store instance
*/
export const showNeuroglassViewer = (store) => {
store.dispatch(setWidgetVisible(widgetsIDs.neuroglassViewerWidgetID, true));
};

/**
* Hide the Neuroglass viewer widget
* @param {Object} store - Redux store instance
*/
export const hideNeuroglassViewer = (store) => {
store.dispatch(setWidgetVisible(widgetsIDs.neuroglassViewerWidgetID, false));
};

/**
* Toggle Neuroglass viewer widget visibility
* @param {Object} store - Redux store instance
*/
export const toggleNeuroglassViewer = (store) => {
const state = store.getState();
const widgets = state.widgets || {};
const neuroglassWidget = widgets[widgetsIDs.neuroglassViewerWidgetID];

const isVisible = neuroglassWidget?.status === 'ACTIVE';
store.dispatch(setWidgetVisible(widgetsIDs.neuroglassViewerWidgetID, !isVisible));
};

/**
* Check if an instance has Neuroglass data available
* @param {string} instanceId - VFB instance ID
* @returns {boolean} True if Neuroglass data exists
*/
export const hasNeuroglassData = (instanceId) => {
return NEUROGLASS_COMPATIBLE_INSTANCES.includes(instanceId) && hasNeuroglasserState(instanceId);
};

/**
* Get the Neuroglancer viewer state for a specific instance
* @param {string} instanceId - VFB instance ID
* @returns {Object|null} Neuroglancer state or null if not available
*/
export const getNeuroglassStateForInstance = (instanceId) => {
return getNeuroglasserState(instanceId);
};

/**
* Auto-show Neuroglass widget when a compatible instance is loaded
* @param {Object} store - Redux store instance
*/
export const autoShowNeuroglass = (store) => {
const state = store.getState();
const loadedInstances = state.instances.allLoadedInstances || [];

const hasCompatibleInstance = loadedInstances.some(
instance => hasNeuroglassData(instance.metadata?.Id)
);

if (hasCompatibleInstance) {
showNeuroglassViewer(store);
}
};
Loading
Loading