-
Notifications
You must be signed in to change notification settings - Fork 1
#vfb-229 - POC: add Neuroglass viewer component and associated state management; integrate into toolbar and layout. #214
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
1f4f2e7
33cf519
d9b1b33
e86ff68
fca42a4
da5d8f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 ''; | ||
|
|
||
| // 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}`; | ||
jrmartin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, [focusedInstance?.metadata?.Id]); | ||
|
|
||
| useEffect(() => { | ||
| if (iframeSrcUrl) { | ||
| setIframeSrc(iframeSrcUrl); | ||
| } | ||
| }, [iframeSrcUrl]); | ||
jrmartin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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" | ||
jrmartin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| /> | ||
| </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 |
|---|---|---|
|
|
@@ -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] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This menu item assumes Useful? React with 👍 / 👎. |
||
| } | ||
| }, | ||
| { | ||
| label: "Template ROI Browser", | ||
| icon: "fa fa-indent", | ||
|
|
||
| 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; | ||
| 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 = { | ||
| 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', | ||
| ]; | ||
jrmartin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * 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)); | ||
jrmartin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| /** | ||
| * 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); | ||
jrmartin marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| /** | ||
| * 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); | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 leavesiframeSrcempty 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 👍 / 👎.