Skip to content

Commit

Permalink
Merge pull request #46 from MetaCell/feature/CELE-29
Browse files Browse the repository at this point in the history
Feature/cele 29
  • Loading branch information
ddelpiano authored Oct 1, 2024
2 parents 018243e + b3607c5 commit 1bd46ae
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 126 deletions.
4 changes: 2 additions & 2 deletions applications/visualizer/backend/visualizer/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@
# DATASET_EMDATA_SEGMENTATION_URL_FORMAT = "resources/{dataset}/em-data/segmentation/{{index}}"


NEURON_REPRESENTATION_3D_URL_FORMAT = "resources/{{dataset}}/3d-model/{name}"
DATASET_NEURON_REPRESENTATION_3D_URL_FORMAT = "resources/{dataset}/3d-model/{{name}}"
NEURON_REPRESENTATION_3D_URL_FORMAT = "resources/{{dataset}}/3d/{name}.stl"
DATASET_NEURON_REPRESENTATION_3D_URL_FORMAT = "resources/{dataset}/3d/{{name}}.stl"
# DATASET_EMDATA_URL_FORMAT = (
# f"resources/sem-adult/catmaid-tiles/{{index}}/{{x}}_{{y}}_{{z}}.jpg"
# )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ interface CustomAutocompleteProps<T> {
ChipProps?: ChipProps;
sx?: SxProps;
componentsProps?: AutocompleteProps<T, boolean, boolean, boolean>["componentsProps"];
value?: T[];
onChange: (v: T | T[]) => void;
value?: T;
onChange: (v: T) => void;
disabled?: boolean;
onInputChange?: (v: string) => void;
}
Expand Down Expand Up @@ -56,7 +56,7 @@ const CommonAutocomplete = <T,>({
disabled={disabled}
onChange={(event: React.SyntheticEvent, value) => {
event.preventDefault();
onChange(value);
onChange(value as T);
}}
clearIcon={clearIcon}
options={options}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Box, InputAdornment, Popper, TextField, Typography } from "@mui/materia
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { CheckIcon } from "../../icons";
import { vars } from "../../theme/variables.ts";
import type { Neuron } from "../../rest/index.ts";
import { vars } from "../../theme/variables.ts";

const { gray50, brand600 } = vars;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Box, IconButton, Stack, Typography } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { useState } from "react";
import { useGlobalContext } from "../../contexts/GlobalContext.tsx";
import { type ViewerData, Visibility } from "../../models/models.ts";
import type { Neuron } from "../../rest";
import { vars } from "../../theme/variables.ts";
import CustomEntitiesDropdown from "./CustomEntitiesDropdown.tsx";
import CustomListItem from "./CustomListItem.tsx";
import { Visibility, type ViewerData } from "../../models/models.ts";

const { gray900, gray500 } = vars;
const mapNeuronsToListItem = (neuron: string, visibility: ViewerData) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import { IconButton, Typography } from "@mui/material";
import type React from "react";
import { CheckIcon, CloseIcon } from "../../../icons";
import type { Dataset } from "../../../rest";
import { vars } from "../../../theme/variables.ts";
import CustomAutocomplete from "../../CustomAutocomplete.tsx";

const { gray100, gray600 } = vars;

interface DatasetPickerProps {
datasets: Dataset[];
selectedDataset: Dataset;
onDatasetChange: (dataset: Dataset) => void;
}

const DatasetPicker: React.FC<DatasetPickerProps> = ({ datasets, selectedDataset, onDatasetChange }) => {
return (
<CustomAutocomplete
multiple={false}
options={datasets}
value={selectedDataset}
onChange={(newValue) => onDatasetChange(newValue)}
getOptionLabel={(option: Dataset) => option.name}
renderOption={(props, option) => (
<li {...props}>
<CheckIcon />
<Typography>{option.name}</Typography>
</li>
)}
placeholder="Start typing to search"
className="secondary"
id="tags-standard"
popupIcon={<KeyboardArrowDownIcon />}
ChipProps={{
deleteIcon: (
<IconButton sx={{ p: "0 !important", margin: "0 !important" }}>
<CloseIcon />
</IconButton>
),
}}
sx={{
position: "absolute",
top: ".5rem",
right: ".5rem",
zIndex: 1,
minWidth: "17.5rem",
"& .MuiInputBase-root": {
padding: "0.5rem 2rem 0.5rem 0.75rem !important",
backgroundColor: gray100,
boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)",
"&.Mui-focused": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: gray100,
boxShadow: "none",
},
},
"& .MuiInputBase-input": {
color: gray600,
fontWeight: 500,
},
},
}}
componentsProps={{
paper: {
sx: {
"& .MuiAutocomplete-listbox": {
"& .MuiAutocomplete-option": {
'&[aria-selected="true"]': {
backgroundColor: "transparent !important",
},
},
},
},
},
}}
/>
);
};

export default DatasetPicker;
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import ZoomInIcon from "@mui/icons-material/ZoomIn";
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
import { Box, Divider, IconButton, Popover, Typography } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { useState } from "react";
import { useRef, useState } from "react";
import { vars } from "../../../theme/variables.ts";
import CustomFormControlLabel from "./CustomFormControlLabel.tsx";

const { gray500 } = vars;

function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);

const rotateAnimationRef = useRef<number | null>(null);
const [isRotating, setIsRotating] = useState(false);
const open = Boolean(anchorEl);
const id = open ? "settings-popover" : undefined;

const handleOpenSettings = (event) => {
setAnchorEl(event.currentTarget);
};
Expand All @@ -22,6 +23,26 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
setAnchorEl(null);
};

const handleRotation = () => {
if (!cameraControlRef.current) return;

const rotate = () => {
cameraControlRef.current.rotate(0.01, 0, true);
rotateAnimationRef.current = requestAnimationFrame(rotate);
};

if (isRotating) {
if (rotateAnimationRef.current) {
cancelAnimationFrame(rotateAnimationRef.current);
rotateAnimationRef.current = null;
}
} else {
rotate();
}

setIsRotating(!isRotating);
};

return (
<Box
sx={{
Expand Down Expand Up @@ -79,7 +100,6 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
3D viewer settings
</Typography>
<CustomFormControlLabel label="Neurons" tooltipTitle="tooltip" helpText="data.helpText" />

<CustomFormControlLabel label="Synapses" tooltipTitle="tooltip" helpText="data.helpText" />
</Box>
</Popover>
Expand Down Expand Up @@ -119,7 +139,7 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) {
</Tooltip>
<Divider />
<Tooltip title="Play 3D viewer" placement="right-start">
<IconButton>
<IconButton onClick={handleRotation}>
<PlayArrowOutlined />
</IconButton>
</Tooltip>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Suspense, useEffect, useRef, useState } from "react";
import { CameraControls, PerspectiveCamera } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { Suspense, useEffect, useMemo, useRef, useState } from "react";
import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts";
import { ViewerType, getNeuronUrlForDataset } from "../../../models/models.ts";
import { type Dataset, OpenAPI } from "../../../rest";
import {
CAMERA_FAR,
CAMERA_FOV,
Expand All @@ -9,24 +14,12 @@ import {
LIGHT_2_POSITION,
SCENE_BACKGROUND,
} from "../../../settings/threeDSettings.ts";

import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import { IconButton, Typography } from "@mui/material";
import { CameraControls, PerspectiveCamera } from "@react-three/drei";
import { Canvas } from "@react-three/fiber";
import { useSelector } from "react-redux";
import { useGlobalContext } from "../../../contexts/GlobalContext.tsx";
import { CheckIcon, CloseIcon } from "../../../icons";
import type { RootState } from "../../../layout-manager/layoutManagerFactory.ts";
import type { Dataset } from "../../../rest";
import { vars } from "../../../theme/variables.ts";
import CustomAutocomplete from "../../CustomAutocomplete.tsx";
import DatasetPicker from "./DatasetPicker.tsx";
import Gizmo from "./Gizmo.tsx";
import Loader from "./Loader.tsx";
import STLViewer from "./STLViewer.tsx";
import SceneControls from "./SceneControls.tsx";

const { gray100, gray600 } = vars;
export interface Instance {
id: string;
url: string;
Expand All @@ -35,100 +28,45 @@ export interface Instance {
}

function ThreeDViewer() {
const workspace = useSelectedWorkspace();
const dataSets = useMemo(() => Object.values(workspace.activeDatasets), [workspace.activeDatasets]);

const [selectedDataset, setSelectedDataset] = useState<Dataset>(dataSets[0]);
const [instances, setInstances] = useState<Instance[]>([]);
const [isWireframe, setIsWireframe] = useState<boolean>(false);

const cameraControlRef = useRef<CameraControls | null>(null);

// @ts-expect-error 'setShowNeurons' is declared but its value is never read.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [showNeurons, setShowNeurons] = useState<boolean>(true);
// @ts-expect-error 'setShowSynapses' is declared but its value is never read.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [showSynapses, setShowSynapses] = useState<boolean>(true);
const [instances, setInstances] = useState<Instance[]>([]);
const [isWireframe, setIsWireframe] = useState<boolean>(false);
const currentWorkspaceId = useSelector((state: RootState) => state.workspaceId);
const { workspaces } = useGlobalContext();
const currentWorkspace = workspaces[currentWorkspaceId];
const cameraControlRef = useRef<CameraControls | null>(null);

useEffect(() => {
if (showNeurons) {
setInstances([
{
id: "nerve_ring",
url: "resources/nervering-SEM_adult.stl",
color: "white",
opacity: 0.3,
},
{
id: "adal_sem",
url: "resources/ADAL-SEM_adult.stl",
color: "blue",
opacity: 1,
},
]);
}
}, [showNeurons, showSynapses]);
if (!selectedDataset) return;

const visibleNeurons = workspace.getVisibleNeuronsInThreeD();
const newInstances: Instance[] = visibleNeurons.flatMap((neuronId) => {
const neuron = workspace.availableNeurons[neuronId];
const viewerData = workspace.visibilities[neuronId]?.[ViewerType.ThreeD];
const urls = getNeuronUrlForDataset(neuron, selectedDataset.id);

return urls.map((url, index) => ({
id: `${neuronId}-${index}`,
url: `${OpenAPI.BASE}/${url}`,
color: viewerData?.color || "#FFFFFF",
opacity: 1,
}));
});

const dataSets = Object.values(currentWorkspace.activeDatasets);
setInstances(newInstances);
}, [selectedDataset, workspace.availableNeurons, workspace.visibilities]);

return (
<>
<CustomAutocomplete
multiple={false}
options={dataSets}
onChange={(e) => console.log(e)}
getOptionLabel={(option: Dataset) => option.name}
renderOption={(props, option) => (
<li {...props}>
<CheckIcon />
<Typography>{option.name}</Typography>
</li>
)}
placeholder="Start typing to search"
className="secondary"
id="tags-standard"
popupIcon={<KeyboardArrowDownIcon />}
ChipProps={{
deleteIcon: (
<IconButton sx={{ p: "0 !important", margin: "0 !important" }}>
<CloseIcon />
</IconButton>
),
}}
sx={{
position: "absolute",
top: ".5rem",
right: ".5rem",
zIndex: 1,
minWidth: "17.5rem",
"& .MuiInputBase-root": {
padding: "0.5rem 2rem 0.5rem 0.75rem !important",
backgroundColor: gray100,
boxShadow: "0px 1px 2px 0px rgba(16, 24, 40, 0.05)",
"&.Mui-focused": {
"& .MuiOutlinedInput-notchedOutline": {
borderColor: gray100,
boxShadow: "none",
},
},
"& .MuiInputBase-input": {
color: gray600,
fontWeight: 500,
},
},
}}
componentsProps={{
paper: {
sx: {
"& .MuiAutocomplete-listbox": {
"& .MuiAutocomplete-option": {
'&[aria-selected="true"]': {
backgroundColor: "transparent !important",
},
},
},
},
},
}}
/>
<DatasetPicker datasets={dataSets} selectedDataset={selectedDataset} onDatasetChange={setSelectedDataset} />
<Canvas style={{ backgroundColor: SCENE_BACKGROUND }} frameloop={"demand"}>
<Suspense fallback={<Loader />}>
<PerspectiveCamera
Expand Down
Loading

0 comments on commit 1bd46ae

Please sign in to comment.