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

ARG DOMAIN
ARG BE_DOMAIN
ARG NEUROGLASS_DATA_PROTOCOL
ARG NEUROGLASS_DATA_BASE_URL

ENV MODULE_NAME=virtual_fly_brain
ENV WORKERS=2
Expand Down Expand Up @@ -59,8 +61,11 @@ 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}" && \
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,46 @@
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';

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 (hasNeuroglassState(focusedInstance.metadata.Id)) {
stateToUse = getNeuroglassState(focusedInstance.metadata.Id);
}
// Priority 2: Default to template
else {
stateToUse = getNeuroglassState('VFB_00101567');
}

if (!stateToUse) return '';

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

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

const theme = useTheme();
// Matches Layout.jsx: const desktopScreen = useMediaQuery(theme.breakpoints.up('lg'))
const isMobile = !useMediaQuery(theme.breakpoints.up('lg')); // true when < 1200px

// 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) return '';
return `${NEUROGLASS_URL}/new#!${encodeURIComponent(JSON.stringify(state))}`;
}, [allLoadedInstances, focusedInstance, neuroglassView, isMobile]);

useEffect(() => {
if (iframeSrcUrl) {
setIframeSrc(iframeSrcUrl);
}
}, [iframeSrcUrl]);
if (!iframeSrc) 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 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 } from '../../utils/constants';

function updateUrlParameterWithCurrentUrl(param, value, reset) {
const urlObj = new URL(window.location.href);
Expand Down Expand Up @@ -106,6 +107,10 @@ 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) store.dispatch(setNeuroglassView(ngView));

uniqueLoadOrder.forEach(id => {
const isFocusTarget = id === focusTarget;
getInstance(allLoadedInstances, id, isFocusTarget, false);
Expand Down Expand Up @@ -295,6 +300,17 @@ export const urlUpdaterMiddleware = store => next => (action) => {
next(action);
break;
}
case getGlobalTypes.SET_NEUROGLASS_VIEW: {
if (action.payload.view) {
updateUrlParameterWithCurrentUrl(NG_LAYOUT_URL_PARAM, action.payload.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
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 / Neuroglancer ───────────────────────────────────────────────
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