Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
11 changes: 9 additions & 2 deletions applications/virtual-fly-brain/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ FROM ${CLOUDHARNESS_FLASK}

ARG DOMAIN
ARG BE_DOMAIN
ARG NEUROGLASS_DATA_PROTOCOL
ARG NEUROGLASS_DATA_BASE_URL
ARG NEUROGLASS_URL

ENV MODULE_NAME=virtual_fly_brain
ENV WORKERS=2
Expand Down Expand Up @@ -59,8 +62,12 @@ RUN echo "=== Environment Check ===" && \
export VFB_DOMAIN="https://${BE_DOMAIN}" && \
echo "Exported VFB_DOMAIN: $VFB_DOMAIN" && \
echo "========================"
# Pass VFB_DOMAIN to Vite build process explicitly
RUN export VFB_DOMAIN="https://${BE_DOMAIN}" && yarn run build
# Pass VFB_DOMAIN and Neuroglass datasource config to Vite build process explicitly
RUN export VFB_DOMAIN="https://${BE_DOMAIN}" && \
export NEUROGLASS_DATA_PROTOCOL="${NEUROGLASS_DATA_PROTOCOL}" && \
export NEUROGLASS_DATA_BASE_URL="${NEUROGLASS_DATA_BASE_URL}" && \
export NEUROGLASS_URL="${NEUROGLASS_URL}" && \
yarn run build

WORKDIR /usr/src/app
COPY backend/requirements.txt /usr/src/app/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,48 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useSelector } from 'react-redux';
import { Box, Typography } from '@mui/material';
import { getNeuroglassState, hasNeuroglassState } from '../utils/neuroglassStateConfig';
import { Box, Typography, useMediaQuery } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { buildNeuroglassState, resolveNeuroglassLayout } from '../utils/neuroglassStateConfig';

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

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

const focusedInstance = useSelector(state => state.instances?.focusedInstance);
const [debouncedSrc, setDebouncedSrc] = useState('');

const iframeSrcUrl = useMemo(() => {
if (!focusedInstance?.metadata?.Id) return '';
const allLoadedInstances = useSelector(state => state.instances?.allLoadedInstances);
const focusedInstance = useSelector(state => state.instances?.focusedInstance);
const neuroglassView = useSelector(state => state.globalInfo?.neuroglassView);

// Priority 1: Predefined state for focused instance (if available)
let stateToUse = null;
if (hasNeuroglassState(focusedInstance.metadata.Id)) {
stateToUse = getNeuroglassState(focusedInstance.metadata.Id);
}
// Priority 2: Default to template
else {
stateToUse = getNeuroglassState('VFB_00101567');
}
const theme = useTheme();
const isMobile = !useMediaQuery(theme.breakpoints.up('lg'));

if (!stateToUse) return '';

const stateStr = JSON.stringify(stateToUse);
const encodedState = encodeURIComponent(stateStr);
return `${NEUROGLASS_URL}/new#!${encodedState}`;
}, [focusedInstance?.metadata?.Id]);
// Rebuilds whenever instances, focused item, layout preference, or viewport size changes.
const iframeSrc = useMemo(() => {
const layout = resolveNeuroglassLayout(neuroglassView, isMobile);
const state = buildNeuroglassState(
allLoadedInstances,
focusedInstance?.metadata?.Id,
layout,
);
if (!state || !NEUROGLASS_URL) return '';
return `${NEUROGLASS_URL}/new#!${encodeURIComponent(JSON.stringify(state))}`;
}, [allLoadedInstances, focusedInstance?.metadata?.Id, neuroglassView, isMobile]);

useEffect(() => {
if (iframeSrcUrl) {
setIframeSrc(iframeSrcUrl);
}
}, [iframeSrcUrl]);
if (!iframeSrc) {
setDebouncedSrc('');
return;
}
const t = setTimeout(() => setDebouncedSrc(iframeSrc), 300);
return () => clearTimeout(t);
}, [iframeSrc]);

return (
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{iframeSrc ? (
{debouncedSrc ? (
<Box sx={{ flex: 1, border: '1px solid #ccc', borderRadius: 1, overflow: 'hidden' }}>
<iframe
src={iframeSrc}
src={debouncedSrc}
style={{
width: '100%',
height: '100%',
Expand All @@ -55,7 +56,11 @@ export default function NeuroglassViewer() {
</Box>
) : (
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: '#fafafa' }}>
<Typography color="textSecondary">Loading Neuroglass viewer...</Typography>
<Typography color="textSecondary">
{!allLoadedInstances || allLoadedInstances.length === 0
? 'No layers selected to display in the Neuroglass viewer.'
: 'Loading Neuroglass viewer...'}
</Typography>
</Box>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WidgetStatus } from "@metacell/geppetto-meta-client/common/layout/model";
import { widgetsIDs } from './../../components/layout/widgets';

export const minimized = {
threeDCanvasWidget : {
Expand Down Expand Up @@ -71,6 +72,17 @@ export const minimized = {
pos: 6,
defaultPosition: 'RIGHT',
props: { size: { height: 600, width: 300 } }
},

neuroglassViewerWidget : {
id: widgetsIDs.neuroglassViewerWidgetID,
name: "Neuroglass Viewer",
component: "neuroglassViewer",
panelName: "right",
hideOnClose: true,
status: WidgetStatus.HIDDEN,
defaultPosition: 'RIGHT',
props: { size: { height: 600, width: 800 } }
}
}

Expand Down Expand Up @@ -128,6 +140,17 @@ export const imagesWidgets = {
status: WidgetStatus.MINIMIZED,
defaultPosition: 'RIGHT',
props: { size: { height: 600, width: 300 } }
},

neuroglassViewerWidget : {
id: widgetsIDs.neuroglassViewerWidgetID,
name: "Neuroglass Viewer",
component: "neuroglassViewer",
panelName: "right",
hideOnClose: true,
status: WidgetStatus.HIDDEN,
defaultPosition: 'RIGHT',
props: { size: { height: 600, width: 800 } }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const initialStateGlobalReducer = {
misalignedIDs : {},
showSliceDisplay : {},
autoSaveLayout : false,
neuroglassView : null,
};

const GlobalReducer = (state = initialStateGlobalReducer, response) => {
Expand Down Expand Up @@ -112,6 +113,11 @@ const GlobalReducer = (state = initialStateGlobalReducer, response) => {
autoSaveLayout: !state.autoSaveLayout
});
}
case getGlobalTypes.SET_NEUROGLASS_VIEW: {
return Object.assign({}, state, {
neuroglassView: response.payload.view,
});
}
default:
return state;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ export const cameraControlAction = (action) => ({
action : action
}
});

export const setNeuroglassView = (view) => ({
type: getGlobalTypes.SET_NEUROGLASS_VIEW,
payload: { view },
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export const getGlobalTypes = Object.freeze({
RESET_ERRORS : 'RESET_ERRORS',
SHOW_SLICE_DISPLAY : 'SHOW_SLICE_DISPLAY',
MODIFY_SLICE_DISPLAY : 'MODIFY_SLICE_DISPLAY',
CAMERA_EVENT : "CAMERA_EVENT"
CAMERA_EVENT : "CAMERA_EVENT",
SET_NEUROGLASS_VIEW : 'SET_NEUROGLASS_VIEW',
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { addRecentSearch } from '../actions/globals';
import { getQueriesFailure } from '../actions/queries';
import { getQueriesTypes } from '../actions/types/getQueriesTypes';
import { getInstancesTypes } from '../actions/types/getInstancesTypes';
import { setFirstIDLoaded, setAlignTemplates, setTemplateID } from '../actions/globals';
import { getGlobalTypes } from '../actions/types/GlobalTypes';
import { setFirstIDLoaded, setAlignTemplates, setTemplateID, setNeuroglassView } from '../actions/globals';
import { getInstanceByID, get3DMesh, triggerInstanceFailure, setBulkLoadingCount, clearUrlLoadingState, focusInstance, selectInstance } from '../actions/instances';
import * as GeppettoActions from '@metacell/geppetto-meta-client/common/actions';
import { DEFAULT_TEMPLATE_ID } from '../../utils/constants';
import { DEFAULT_TEMPLATE_ID, NG_LAYOUT_URL_PARAM, KNOWN_NG_VIEWS } from '../../utils/constants';

function updateUrlParameterWithCurrentUrl(param, value, reset) {
const urlObj = new URL(window.location.href);
Expand Down Expand Up @@ -106,6 +107,15 @@ const isFirstTimeLoad = (allLoadedInstances, store) => {
updateUrlParameterWithCurrentUrl('id', DEFAULT_ID, true);
}

// Read ?layout param and sync into Redux
const ngView = getUrlParameter(NG_LAYOUT_URL_PARAM);
if (ngView && Array.isArray(KNOWN_NG_VIEWS) && KNOWN_NG_VIEWS.includes(ngView)) {
store.dispatch(setNeuroglassView(ngView));
} else if (ngView) {
// Clear unrecognized layout parameter from URL to avoid propagating invalid values
updateUrlParameterWithCurrentUrl(NG_LAYOUT_URL_PARAM, '', true);
}

uniqueLoadOrder.forEach(id => {
const isFocusTarget = id === focusTarget;
getInstance(allLoadedInstances, id, isFocusTarget, false);
Expand Down Expand Up @@ -295,6 +305,19 @@ export const urlUpdaterMiddleware = store => next => (action) => {
next(action);
break;
}
case getGlobalTypes.SET_NEUROGLASS_VIEW: {
const view = action.payload.view;
const isValidView = typeof view === 'string' && view.trim().length > 0;
if (isValidView) {
updateUrlParameterWithCurrentUrl(NG_LAYOUT_URL_PARAM, view, true);
} else {
const urlObj = new URL(window.location.href);
urlObj.searchParams.delete(NG_LAYOUT_URL_PARAM);
window.history.replaceState(null, '', decodeURIComponent(urlObj.toString()));
}
next(action);
break;
}
default:
next(action);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const vfbMiddleware = store => next => (action) => {
keys.push(widgetsIDs.termContextWidgetID);
keys.push(widgetsIDs.roiBrowserWidgetID);
keys.push(widgetsIDs.listViewerWidgetID);
keys.push(widgetsIDs.neuroglassViewerWidgetID);
}
const layoutManager = getLayoutManagerInstance();
const activePanel = layoutManager.model.getRoot().getModel().getActiveTabset().getId();
Expand Down
10 changes: 10 additions & 0 deletions applications/virtual-fly-brain/frontend/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ export const hexToRGBA = (hexColor) => {
return { r, g, b, a: 1 };
}

// Neuroglass viewer integration constants and utilities
export const KNOWN_NG_VIEWS = Object.freeze([
'4panel-alt', '4panel', // all quadrants
'3d', 'xy', 'xz', 'yz', // single panel fullscreen
'xy-3d', 'xz-3d', 'yz-3d', // slice + volumetric side-by-side
]);
export const NG_LAYOUT_URL_PARAM = 'layout';
export const NG_DEFAULT_LAYOUT = '4panel-alt';
export const NG_DEFAULT_MOBILE_LAYOUT = '3d';

export const RGBAToHexA = (color) => {
let r = Math.round(color?.r * 255).toString(16);
let g = Math.round(color?.g * 255).toString(16);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import { setWidgetVisible } from '@metacell/geppetto-meta-client/common/layout/actions';
import { WidgetStatus } from '@metacell/geppetto-meta-client/common/layout/model';
import { widgetsIDs } from '../components/layout/widgets';
import { hasNeuroglassState, getNeuroglassState, NEUROGLASS_STATES_MAP } from './neuroglassStateConfig';


/**
Expand Down Expand Up @@ -38,37 +37,3 @@ export const toggleNeuroglassViewer = (store) => {
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 hasNeuroglassState(instanceId);
};

/**
* Get the Neuroglass viewer state for a specific instance
* @param {string} instanceId - VFB instance ID
* @returns {Object|null} Neuroglass state or null if not available
*/
export const getNeuroglassStateForInstance = (instanceId) => {
return getNeuroglassState(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