From 363f7e460a3c5df0e383aa09fa36d308a60f369a Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 18 Sep 2024 17:22:43 +0100 Subject: [PATCH 1/7] CELE-29 feat: Connect threeD viewer --- .../backend/visualizer/settings/common.py | 4 +- .../src/components/CustomAutocomplete.tsx | 4 +- .../viewers/ThreeD/DatasetPicker.tsx | 81 +++++++++++ .../components/viewers/ThreeD/STLViewer.tsx | 1 + .../viewers/ThreeD/ThreeDViewer.tsx | 133 +++++------------- .../components/viewers/TwoD/ContextMenu.tsx | 10 +- .../src/helpers/threeD/threeDHelpers.ts | 8 ++ .../src/helpers/twoD/splitJoinHelper.ts | 6 +- .../frontend/src/helpers/twoD/twoDHelpers.ts | 4 +- .../visualizer/frontend/src/models/models.ts | 14 +- .../frontend/src/models/workspace.ts | 14 +- 11 files changed, 163 insertions(+), 116 deletions(-) create mode 100644 applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx create mode 100644 applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts diff --git a/applications/visualizer/backend/visualizer/settings/common.py b/applications/visualizer/backend/visualizer/settings/common.py index d29bc555..628a69ce 100644 --- a/applications/visualizer/backend/visualizer/settings/common.py +++ b/applications/visualizer/backend/visualizer/settings/common.py @@ -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}}" # DATASET_EMDATA_URL_FORMAT = ( # f"resources/sem-adult/catmaid-tiles/{{index}}/{{x}}_{{y}}_{{z}}.jpg" # ) diff --git a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx index 2b52570c..4866f00b 100644 --- a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx +++ b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx @@ -20,8 +20,8 @@ interface CustomAutocompleteProps { ChipProps?: ChipProps; sx?: SxProps; componentsProps?: AutocompleteProps["componentsProps"]; - value?: T[]; - onChange: (v: T | T[]) => void; + value?: T; + onChange: (v: T ) => void; disabled?: boolean; onInputChange?: (v: string) => void; } diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx new file mode 100644 index 00000000..6799b639 --- /dev/null +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { Dataset } from '../../../rest'; +import CustomAutocomplete from "../../CustomAutocomplete.tsx"; +import { Typography, IconButton } from '@mui/material'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { CheckIcon, CloseIcon } from "../../../icons"; +import { vars } from "../../../theme/variables.ts"; + +const { gray100, gray600 } = vars; + +interface DatasetPickerProps { + datasets: Dataset[]; + selectedDataset: Dataset; + onDatasetChange: (dataset: Dataset) => void; +} + +const DatasetPicker: React.FC = ({ datasets, selectedDataset, onDatasetChange }) => { + return ( + onDatasetChange(newValue)} + getOptionLabel={(option: Dataset) => option.name} + renderOption={(props, option) => ( +
  • + + {option.name} +
  • + )} + placeholder="Start typing to search" + className="secondary" + id="tags-standard" + popupIcon={} + ChipProps={{ + deleteIcon: ( + + + + ), + }} + 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; \ No newline at end of file diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx index 982365e5..3e6b7fb4 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx @@ -12,6 +12,7 @@ interface Props { } const STLViewer: FC = ({ instances, isWireframe }) => { + // TODO: Check if useLoader caches or do we need to do it ourselves // @ts-expect-error Argument type STLLoader is not assignable to parameter type LoaderProto const stlObjects = useLoader( diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx index 2ef4dd95..75c6322f 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -1,4 +1,4 @@ -import { Suspense, useEffect, useRef, useState } from "react"; +import { Suspense, useEffect, useMemo, useRef, useState } from "react"; import { CAMERA_FAR, CAMERA_FOV, @@ -9,24 +9,18 @@ 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 { OpenAPI, type Dataset } from "../../../rest"; import Gizmo from "./Gizmo.tsx"; import Loader from "./Loader.tsx"; import STLViewer from "./STLViewer.tsx"; import SceneControls from "./SceneControls.tsx"; +import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts"; +import DatasetPicker from "./DatasetPicker.tsx"; +import { getVisibleNeuronsInThreeD } from "../../../helpers/threeD/threeDHelpers.ts"; +import { getNeuronUrlForDataset, ViewerType } from "../../../models/models.ts"; -const { gray100, gray600 } = vars; export interface Instance { id: string; url: string; @@ -35,99 +29,50 @@ export interface Instance { } function ThreeDViewer() { + + const workspace = useSelectedWorkspace(); + const dataSets = useMemo(() => Object.values(workspace.activeDatasets), [workspace.activeDatasets]); + + const [selectedDataset, setSelectedDataset] = useState(dataSets[0]); + const [instances, setInstances] = useState([]); + const [isWireframe, setIsWireframe] = useState(false); + + const cameraControlRef = useRef(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(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(true); - const [instances, setInstances] = useState([]); - const [isWireframe, setIsWireframe] = useState(false); - const currentWorkspaceId = useSelector((state: RootState) => state.workspaceId); - const { workspaces } = useGlobalContext(); - const currentWorkspace = workspaces[currentWorkspaceId]; - const cameraControlRef = useRef(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]); - const dataSets = Object.values(currentWorkspace.activeDatasets); + useEffect(() => { + if (!selectedDataset) return; + + const visibleNeurons = getVisibleNeuronsInThreeD(workspace); + 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, + })); + }); + + setInstances(newInstances); + }, [selectedDataset, workspace.availableNeurons, workspace.visibilities]); return ( <> - console.log(e)} - getOptionLabel={(option: Dataset) => option.name} - renderOption={(props, option) => ( -
  • - - {option.name} -
  • - )} - placeholder="Start typing to search" - className="secondary" - id="tags-standard" - popupIcon={} - ChipProps={{ - deleteIcon: ( - - - - ), - }} - 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", - }, - }, - }, - }, - }, - }} + }> diff --git a/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx b/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx index f1fabc9f..caedd588 100644 --- a/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx +++ b/applications/visualizer/frontend/src/components/viewers/TwoD/ContextMenu.tsx @@ -21,7 +21,7 @@ import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts"; import { AlignBottomIcon, AlignLeftIcon, AlignRightIcon, AlignTopIcon, DistributeHorizontallyIcon, DistributeVerticallyIcon } from "../../../icons"; import { Alignment, ViewerType, Visibility } from "../../../models"; import { vars } from "../../../theme/variables.ts"; -import { emptyViewerData } from "../../../models/models.ts"; +import { getDefaultViewerData } from "../../../models/models.ts"; const { gray700 } = vars; @@ -70,7 +70,7 @@ const ContextMenu: React.FC = ({ open, onClose, position, setS workspace.customUpdate((draft) => { for (const neuronId of workspace.selectedNeurons) { if (!(neuronId in draft.visibilities)) { - draft.visibilities[neuronId] = emptyViewerData(Visibility.Hidden); + draft.visibilities[neuronId] = getDefaultViewerData(Visibility.Hidden); } else { draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Hidden; } @@ -86,7 +86,7 @@ const ContextMenu: React.FC = ({ open, onClose, position, setS workspace.customUpdate((draft) => { // Add the new group draft.neuronGroups[newGroupId] = newGroup; - draft.visibilities[newGroupId] = emptyViewerData(Visibility.Visible); + draft.visibilities[newGroupId] = getDefaultViewerData(Visibility.Visible); // Remove the old groups that were merged into the new group for (const groupId of groupsToDelete) { @@ -194,11 +194,11 @@ const ContextMenu: React.FC = ({ open, onClose, position, setS if (group) { for (const groupedNeuronId of group.neurons) { draft.activeNeurons.add(groupedNeuronId); - draft.visibilities[groupedNeuronId] = emptyViewerData(Visibility.Visible); + draft.visibilities[groupedNeuronId] = getDefaultViewerData(Visibility.Visible); } } else { draft.activeNeurons.add(neuronId); - draft.visibilities[neuronId] = emptyViewerData(Visibility.Visible); + draft.visibilities[neuronId] = getDefaultViewerData(Visibility.Visible); } } }); diff --git a/applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts b/applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts new file mode 100644 index 00000000..cfe7678a --- /dev/null +++ b/applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts @@ -0,0 +1,8 @@ +import { ViewerType, Visibility, type Workspace } from "../../models"; + +export function getVisibleNeuronsInThreeD(workspace: Workspace): string[] { + return Array.from(workspace.activeNeurons).filter(neuronId => + workspace.visibilities[neuronId]?.[ViewerType.ThreeD]?.visibility === Visibility.Visible + ); + } + \ No newline at end of file diff --git a/applications/visualizer/frontend/src/helpers/twoD/splitJoinHelper.ts b/applications/visualizer/frontend/src/helpers/twoD/splitJoinHelper.ts index b90800a1..8189ae8e 100644 --- a/applications/visualizer/frontend/src/helpers/twoD/splitJoinHelper.ts +++ b/applications/visualizer/frontend/src/helpers/twoD/splitJoinHelper.ts @@ -1,5 +1,5 @@ import { ViewerType, type Workspace } from "../../models"; -import { emptyViewerData, type GraphViewerData, Visibility } from "../../models/models.ts"; +import { getDefaultViewerData, type GraphViewerData, Visibility } from "../../models/models.ts"; import { calculateMeanPosition, calculateSplitPositions, isNeuronCell, isNeuronClass } from "./twoDHelpers.ts"; interface SplitJoinState { @@ -68,7 +68,7 @@ export const processNeuronSplit = (workspace: Workspace, splitJoinState: SplitJo for (const [neuronName, update] of Object.entries(graphViewDataUpdates)) { if (!(neuronName in draft.visibilities)) { - draft.visibilities[neuronName] = emptyViewerData(update.visibility); + draft.visibilities[neuronName] = getDefaultViewerData(update.visibility); } if (update.defaultPosition !== undefined) { draft.visibilities[neuronName][ViewerType.Graph].defaultPosition = update.defaultPosition; @@ -145,7 +145,7 @@ export const processNeuronJoin = (workspace: Workspace, splitJoinState: SplitJoi for (const [neuronName, update] of Object.entries(graphViewDataUpdates)) { if (!(neuronName in draft.visibilities)) { - draft.visibilities[neuronName] = emptyViewerData(update.visibility); + draft.visibilities[neuronName] = getDefaultViewerData(update.visibility); } if (update.defaultPosition !== undefined) { draft.visibilities[neuronName][ViewerType.Graph].defaultPosition = update.defaultPosition; diff --git a/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts b/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts index 39bd0ec4..aace9eb4 100644 --- a/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts +++ b/applications/visualizer/frontend/src/helpers/twoD/twoDHelpers.ts @@ -3,7 +3,7 @@ import { ViewerType, Visibility, type Workspace } from "../../models"; import type { Connection } from "../../rest"; import { GRAPH_LAYOUTS, LAYOUT_OPTIONS, annotationLegend } from "../../settings/twoDSettings.tsx"; import { cellConfig, neurotransmitterConfig } from "./coloringHelper.ts"; -import { emptyViewerData } from "../../models/models.ts"; +import { getDefaultViewerData } from "../../models/models.ts"; import { getConcentricLayoutPositions } from "./concentricLayoutHelper.ts"; export const createEdge = (id: string, conn: Connection, workspace: Workspace, includeAnnotations: boolean, width: number): ElementDefinition => { @@ -228,7 +228,7 @@ export const updateWorkspaceNeurons2DViewerData = (workspace: Workspace, cy: Cor for (const node of cy.nodes()) { const neuronId = node.id(); if (!(neuronId in draft.visibilities)) { - draft.visibilities[neuronId] = emptyViewerData(Visibility.Visible); + draft.visibilities[neuronId] = getDefaultViewerData(Visibility.Visible); } draft.visibilities[neuronId][ViewerType.Graph].defaultPosition = { ...node.position() }; draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Visible; diff --git a/applications/visualizer/frontend/src/models/models.ts b/applications/visualizer/frontend/src/models/models.ts index aaa74055..5eb0305f 100644 --- a/applications/visualizer/frontend/src/models/models.ts +++ b/applications/visualizer/frontend/src/models/models.ts @@ -31,18 +31,28 @@ export interface GraphViewerData { visibility: Visibility; } -export function emptyViewerData(visibility?: Visibility): ViewerData { +export interface ThreeDViewerData { + visibility: Visibility; + color: string; +} + + +export function getDefaultViewerData(visibility?: Visibility): ViewerData { return { [ViewerType.Graph]: { defaultPosition: null, visibility: visibility ?? Visibility.Hidden, }, + [ViewerType.ThreeD]: { + visibility: visibility ?? Visibility.Hidden, + color: "#000000", + }, }; } export interface ViewerData { [ViewerType.Graph]?: GraphViewerData; - [ViewerType.ThreeD]?: any; // Define specific data for 3D viewer if needed + [ViewerType.ThreeD]?: ThreeDViewerData; [ViewerType.EM]?: any; // Define specific data for EM viewer if needed [ViewerType.InstanceDetails]?: any; // Define specific data for Instance Details viewer if needed } diff --git a/applications/visualizer/frontend/src/models/workspace.ts b/applications/visualizer/frontend/src/models/workspace.ts index 71071344..2ceb20f5 100644 --- a/applications/visualizer/frontend/src/models/workspace.ts +++ b/applications/visualizer/frontend/src/models/workspace.ts @@ -4,7 +4,7 @@ import { immerable, produce } from "immer"; import getLayoutManagerAndStore from "../layout-manager/layoutManagerFactory"; import { type Dataset, type Neuron, NeuronsService } from "../rest"; import { GlobalError } from "./Error.ts"; -import { emptyViewerData, type NeuronGroup, type ViewerData, type ViewerSynchronizationPair, ViewerType, Visibility } from "./models"; +import { getDefaultViewerData, type NeuronGroup, type ViewerData, type ViewerSynchronizationPair, ViewerType, Visibility } from "./models"; import { type SynchronizerContext, SynchronizerOrchestrator } from "./synchronizer"; export class Workspace { @@ -58,7 +58,7 @@ export class Workspace { this.layoutManager = layoutManager; this.syncOrchestrator = SynchronizerOrchestrator.create(activeSynchronizers, contexts); - this.visibilities = visibilities || Object.fromEntries([...activeNeurons].map((n) => [n, emptyViewerData(Visibility.Visible)])); + this.visibilities = visibilities || Object.fromEntries([...activeNeurons].map((n) => [n, getDefaultViewerData(Visibility.Visible)])); this.store = store; this.updateContext = updateContext; @@ -69,7 +69,7 @@ export class Workspace { activateNeuron(neuron: Neuron): Workspace { const updated = produce(this, (draft: Workspace) => { draft.activeNeurons.add(neuron.name); - draft.visibilities[neuron.name] = emptyViewerData(); + draft.visibilities[neuron.name] = getDefaultViewerData(); }); this.updateContext(updated); return updated; @@ -86,10 +86,11 @@ export class Workspace { hideNeuron(neuronId: string): void { const updated = produce(this, (draft: Workspace) => { if (!(neuronId in draft.visibilities)) { - draft.visibilities[neuronId] = emptyViewerData(Visibility.Hidden); + draft.visibilities[neuronId] = getDefaultViewerData(Visibility.Hidden); } // todo: add actions for other viewers draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Hidden; + draft.visibilities[neuronId][ViewerType.ThreeD].visibility = Visibility.Hidden; }); this.updateContext(updated); } @@ -97,10 +98,11 @@ export class Workspace { showNeuron(neuronId: string): void { const updated = produce(this, (draft: Workspace) => { if (!(neuronId in draft.visibilities)) { - draft.visibilities[neuronId] = emptyViewerData(Visibility.Visible); + draft.visibilities[neuronId] = getDefaultViewerData(Visibility.Visible); } // todo: add actions for other viewers draft.visibilities[neuronId][ViewerType.Graph].visibility = Visibility.Visible; + draft.visibilities[neuronId][ViewerType.ThreeD].visibility = Visibility.Visible; }); this.updateContext(updated); @@ -219,7 +221,7 @@ export class Workspace { const className = neuron.nclass; if (!(className in neuronsClass)) { - const neuronClass = { ...neuron, name: className }; + const neuronClass = { ...neuron, name: className, model3DUrls: [...neuron.model3DUrls], datasetIds: [...neuron.datasetIds] }; neuronsClass[className] = neuronClass; uniqueNeurons.add(neuronClass); } else { From 0f5b44999f5560f655af060a88bce398c11f3c8c Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 18 Sep 2024 17:32:55 +0100 Subject: [PATCH 2/7] CELE-29 style: Apply formatting --- .../src/components/CustomAutocomplete.tsx | 2 +- .../components/viewers/ThreeD/DatasetPicker.tsx | 10 +++++----- .../src/components/viewers/ThreeD/STLViewer.tsx | 1 - .../components/viewers/ThreeD/ThreeDViewer.tsx | 16 +++++----------- .../frontend/src/helpers/threeD/threeDHelpers.ts | 7 ++----- .../visualizer/frontend/src/models/models.ts | 1 - 6 files changed, 13 insertions(+), 24 deletions(-) diff --git a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx index 4866f00b..f5dddeec 100644 --- a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx +++ b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx @@ -21,7 +21,7 @@ interface CustomAutocompleteProps { sx?: SxProps; componentsProps?: AutocompleteProps["componentsProps"]; value?: T; - onChange: (v: T ) => void; + onChange: (v: T) => void; disabled?: boolean; onInputChange?: (v: string) => void; } diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx index 6799b639..7d3bbdeb 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { Dataset } from '../../../rest'; +import type React from "react"; +import type { Dataset } from "../../../rest"; import CustomAutocomplete from "../../CustomAutocomplete.tsx"; -import { Typography, IconButton } from '@mui/material'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import { Typography, IconButton } from "@mui/material"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import { CheckIcon, CloseIcon } from "../../../icons"; import { vars } from "../../../theme/variables.ts"; @@ -78,4 +78,4 @@ const DatasetPicker: React.FC = ({ datasets, selectedDataset ); }; -export default DatasetPicker; \ No newline at end of file +export default DatasetPicker; diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx index 3e6b7fb4..982365e5 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx @@ -12,7 +12,6 @@ interface Props { } const STLViewer: FC = ({ instances, isWireframe }) => { - // TODO: Check if useLoader caches or do we need to do it ourselves // @ts-expect-error Argument type STLLoader is not assignable to parameter type LoaderProto const stlObjects = useLoader( diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx index 75c6322f..3717e28e 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -29,7 +29,6 @@ export interface Instance { } function ThreeDViewer() { - const workspace = useSelectedWorkspace(); const dataSets = useMemo(() => Object.values(workspace.activeDatasets), [workspace.activeDatasets]); @@ -46,16 +45,15 @@ function ThreeDViewer() { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [showSynapses, setShowSynapses] = useState(true); - useEffect(() => { if (!selectedDataset) return; - + const visibleNeurons = getVisibleNeuronsInThreeD(workspace); - const newInstances: Instance[] = visibleNeurons.flatMap(neuronId => { + 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}`, @@ -63,17 +61,13 @@ function ThreeDViewer() { opacity: 1, })); }); - + setInstances(newInstances); }, [selectedDataset, workspace.availableNeurons, workspace.visibilities]); return ( <> - + }> - workspace.visibilities[neuronId]?.[ViewerType.ThreeD]?.visibility === Visibility.Visible - ); - } - \ No newline at end of file + return Array.from(workspace.activeNeurons).filter((neuronId) => workspace.visibilities[neuronId]?.[ViewerType.ThreeD]?.visibility === Visibility.Visible); +} diff --git a/applications/visualizer/frontend/src/models/models.ts b/applications/visualizer/frontend/src/models/models.ts index 5eb0305f..c5c769a7 100644 --- a/applications/visualizer/frontend/src/models/models.ts +++ b/applications/visualizer/frontend/src/models/models.ts @@ -36,7 +36,6 @@ export interface ThreeDViewerData { color: string; } - export function getDefaultViewerData(visibility?: Visibility): ViewerData { return { [ViewerType.Graph]: { From 5708b24fc5f7ced2233a237bef4950d97357ce11 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 18 Sep 2024 17:43:46 +0100 Subject: [PATCH 3/7] CELE-29 fix: Fix type mismatch --- .../src/components/CustomAutocomplete.tsx | 2 +- .../frontend/src/models/synchronizer.ts | 16 ++++++++-------- .../visualizer/frontend/src/models/workspace.ts | 6 ++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx index f5dddeec..37b5dccb 100644 --- a/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx +++ b/applications/visualizer/frontend/src/components/CustomAutocomplete.tsx @@ -56,7 +56,7 @@ const CommonAutocomplete = ({ disabled={disabled} onChange={(event: React.SyntheticEvent, value) => { event.preventDefault(); - onChange(value); + onChange(value as T); }} clearIcon={clearIcon} options={options} diff --git a/applications/visualizer/frontend/src/models/synchronizer.ts b/applications/visualizer/frontend/src/models/synchronizer.ts index 2b1f6034..eb115b65 100644 --- a/applications/visualizer/frontend/src/models/synchronizer.ts +++ b/applications/visualizer/frontend/src/models/synchronizer.ts @@ -44,34 +44,34 @@ class Synchronizer { } } - select(selection: EnhancedNeuron, initiator: ViewerType, contexts: Record) { + select(selection: string, initiator: ViewerType, contexts: Record) { if (!this.canHandle(initiator)) { return; } if (!this.active) { - contexts[initiator] = [...new Set([...contexts[initiator], selection.name])]; + contexts[initiator] = [...new Set([...contexts[initiator], selection])]; return; } for (const viewer of this.viewers) { - contexts[viewer] = [...new Set([...contexts[viewer], selection.name])]; + contexts[viewer] = [...new Set([...contexts[viewer], selection])]; } } - unSelect(selection: EnhancedNeuron, initiator: ViewerType, contexts: Record) { + unSelect(selection: string, initiator: ViewerType, contexts: Record) { if (!this.canHandle(initiator)) { return; } if (!this.active) { const storedNodes = [...contexts[initiator]]; - contexts[initiator] = storedNodes.filter((n) => n !== selection.name); + contexts[initiator] = storedNodes.filter((n) => n !== selection); return; } for (const viewer of this.viewers) { const storedNodes = [...contexts[viewer]]; - contexts[viewer] = storedNodes.filter((n) => n !== selection.name); + contexts[viewer] = storedNodes.filter((n) => n !== selection); } } @@ -129,13 +129,13 @@ export class SynchronizerOrchestrator { } } - public selectNeuron(selection: EnhancedNeuron, initiator: ViewerType) { + public selectNeuron(selection: string, initiator: ViewerType) { for (const synchronizer of this.synchronizers) { synchronizer.select(selection, initiator, this.contexts); } } - public unSelectNeuron(selection: EnhancedNeuron, initiator: ViewerType) { + public unSelectNeuron(selection: string, initiator: ViewerType) { for (const synchronizer of this.synchronizers) { synchronizer.unSelect(selection, initiator, this.contexts); } diff --git a/applications/visualizer/frontend/src/models/workspace.ts b/applications/visualizer/frontend/src/models/workspace.ts index 2ceb20f5..2e4a635c 100644 --- a/applications/visualizer/frontend/src/models/workspace.ts +++ b/applications/visualizer/frontend/src/models/workspace.ts @@ -255,16 +255,14 @@ export class Workspace { } addSelection(selection: string, initiator: ViewerType) { - const selectedNeurons = this.availableNeurons[selection]; this.customUpdate((draft) => { - draft.syncOrchestrator.selectNeuron(selectedNeurons, initiator); + draft.syncOrchestrator.selectNeuron(selection, initiator); }); } removeSelection(selection: string, initiator: ViewerType) { - const selectedNeurons = this.availableNeurons[selection]; this.customUpdate((draft) => { - draft.syncOrchestrator.unSelectNeuron(selectedNeurons, initiator); + draft.syncOrchestrator.unSelectNeuron(selection, initiator); }); } From 8e05ed9145fdc051c7d0b8f538cf2290bdd857f1 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 18 Sep 2024 20:04:52 +0100 Subject: [PATCH 4/7] CELE-29 refactor: Move helper to workspace --- .../visualizer/backend/visualizer/settings/common.py | 2 +- .../frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx | 2 +- .../visualizer/frontend/src/helpers/threeD/threeDHelpers.ts | 5 ----- applications/visualizer/frontend/src/models/workspace.ts | 4 ++++ 4 files changed, 6 insertions(+), 7 deletions(-) delete mode 100644 applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts diff --git a/applications/visualizer/backend/visualizer/settings/common.py b/applications/visualizer/backend/visualizer/settings/common.py index 628a69ce..b9c2ad3b 100644 --- a/applications/visualizer/backend/visualizer/settings/common.py +++ b/applications/visualizer/backend/visualizer/settings/common.py @@ -154,7 +154,7 @@ NEURON_REPRESENTATION_3D_URL_FORMAT = "resources/{{dataset}}/3d/{name}.stl" -DATASET_NEURON_REPRESENTATION_3D_URL_FORMAT = "resources/{dataset}/3d/{{name}}" +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" # ) diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx index 3717e28e..753da08a 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -48,7 +48,7 @@ function ThreeDViewer() { useEffect(() => { if (!selectedDataset) return; - const visibleNeurons = getVisibleNeuronsInThreeD(workspace); + const visibleNeurons = workspace.getVisibleNeuronsInThreeD(); const newInstances: Instance[] = visibleNeurons.flatMap((neuronId) => { const neuron = workspace.availableNeurons[neuronId]; const viewerData = workspace.visibilities[neuronId]?.[ViewerType.ThreeD]; diff --git a/applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts b/applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts deleted file mode 100644 index f5a9aec1..00000000 --- a/applications/visualizer/frontend/src/helpers/threeD/threeDHelpers.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ViewerType, Visibility, type Workspace } from "../../models"; - -export function getVisibleNeuronsInThreeD(workspace: Workspace): string[] { - return Array.from(workspace.activeNeurons).filter((neuronId) => workspace.visibilities[neuronId]?.[ViewerType.ThreeD]?.visibility === Visibility.Visible); -} diff --git a/applications/visualizer/frontend/src/models/workspace.ts b/applications/visualizer/frontend/src/models/workspace.ts index 2e4a635c..2ff54cec 100644 --- a/applications/visualizer/frontend/src/models/workspace.ts +++ b/applications/visualizer/frontend/src/models/workspace.ts @@ -276,4 +276,8 @@ export class Workspace { const neuron = this.availableNeurons[neuronId]; return neuron.nclass; } + + getVisibleNeuronsInThreeD(): string[] { + return Array.from(this.activeNeurons).filter((neuronId) => this.visibilities[neuronId]?.[ViewerType.ThreeD]?.visibility === Visibility.Visible); + } } From c689578f6259061316777bd8952a53b87a1abf78 Mon Sep 17 00:00:00 2001 From: afonso Date: Wed, 18 Sep 2024 20:05:28 +0100 Subject: [PATCH 5/7] CELE-29 style: Apply formatting --- applications/visualizer/frontend/src/models/workspace.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/visualizer/frontend/src/models/workspace.ts b/applications/visualizer/frontend/src/models/workspace.ts index 2ff54cec..5e2f5ef6 100644 --- a/applications/visualizer/frontend/src/models/workspace.ts +++ b/applications/visualizer/frontend/src/models/workspace.ts @@ -276,7 +276,7 @@ export class Workspace { const neuron = this.availableNeurons[neuronId]; return neuron.nclass; } - + getVisibleNeuronsInThreeD(): string[] { return Array.from(this.activeNeurons).filter((neuronId) => this.visibilities[neuronId]?.[ViewerType.ThreeD]?.visibility === Visibility.Visible); } From 586559a82302179c5f4f35c6c934856c14e3dc04 Mon Sep 17 00:00:00 2001 From: aranega Date: Wed, 18 Sep 2024 15:04:06 -0600 Subject: [PATCH 6/7] CELE-29 Fix build --- .../src/components/viewers/ThreeD/ThreeDViewer.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx index 753da08a..5aa9e3a4 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -1,4 +1,9 @@ +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, @@ -9,17 +14,11 @@ import { LIGHT_2_POSITION, SCENE_BACKGROUND, } from "../../../settings/threeDSettings.ts"; -import { CameraControls, PerspectiveCamera } from "@react-three/drei"; -import { Canvas } from "@react-three/fiber"; -import { OpenAPI, type Dataset } from "../../../rest"; +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"; -import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts"; -import DatasetPicker from "./DatasetPicker.tsx"; -import { getVisibleNeuronsInThreeD } from "../../../helpers/threeD/threeDHelpers.ts"; -import { getNeuronUrlForDataset, ViewerType } from "../../../models/models.ts"; export interface Instance { id: string; From d4baba8ac438de345711a447ade54fbfe13eacf0 Mon Sep 17 00:00:00 2001 From: salam dalloul Date: Thu, 26 Sep 2024 22:06:10 +0200 Subject: [PATCH 7/7] #95 3D viewer auto-rotation --- .../viewers/ThreeD/SceneControls.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/SceneControls.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/SceneControls.tsx index 765f0bb8..d1866c96 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/SceneControls.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/SceneControls.tsx @@ -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(null); - + const rotateAnimationRef = useRef(null); + const [isRotating, setIsRotating] = useState(false); const open = Boolean(anchorEl); const id = open ? "settings-popover" : undefined; + const handleOpenSettings = (event) => { setAnchorEl(event.currentTarget); }; @@ -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 ( - @@ -119,7 +139,7 @@ function SceneControls({ cameraControlRef, isWireframe, setIsWireframe }) { - +