diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx index c7c83ac..1a30683 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/DatasetPicker.tsx @@ -23,7 +23,7 @@ const DatasetPicker: React.FC = ({ datasets, selectedDataset onChange={(newValue) => onDatasetChange(newValue)} getOptionLabel={(option: Dataset) => option.name} renderOption={(props, option) => ( -
  • +
  • {option.name}
  • diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx index 982365e..563bcee 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/STLViewer.tsx @@ -1,8 +1,8 @@ import { Center } from "@react-three/drei"; -import { useLoader } from "@react-three/fiber"; -import type { FC } from "react"; -import type { BufferGeometry } from "three"; +import { type FC, useMemo, useState } from "react"; import { STLLoader } from "three/examples/jsm/loaders/STLLoader"; +import { useGlobalContext } from "../../../contexts/GlobalContext.tsx"; +import { GlobalError } from "../../../models/Error.ts"; import STLMesh from "./STLMesh.tsx"; import type { Instance } from "./ThreeDViewer.tsx"; @@ -12,12 +12,48 @@ 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( - STLLoader, - instances.map((i) => i.url), - ); + const { handleErrors } = useGlobalContext(); + const [stlObjects, setSTLObjects] = useState([]); + + useMemo(() => { + const loader = new STLLoader(); + + const errorFiles = []; + + // Load all STL files in parallel + const loadSTLFiles = async () => { + const loadPromises = instances.map( + (instance) => + new Promise((resolve, _) => { + loader.load( + instance.url, + (geometry) => resolve(geometry.center()), + undefined, + (error) => { + console.error(`Error loading ${instance.url}:`, error); + errorFiles.push(instance); + resolve(null); + }, + ); + }), + ); + // Wait for all promises to finish + const results = await Promise.allSettled(loadPromises); + + // We filter now all the promises that didn't finish properly + // @ts-expect-error + const successfulModels = results.filter((result) => result.status === "fulfilled" && result.value).map((result) => result.value); + + setSTLObjects(successfulModels); + + // If there is some error, we display a message to inform the user + if (errorFiles.length > 0) { + handleErrors(new GlobalError(`Couldn't fetch 3D representation for ${errorFiles.map((e) => e.id)}`)); + } + }; + + loadSTLFiles(); + }, [instances]); return (
    @@ -26,7 +62,6 @@ const STLViewer: FC = ({ instances, isWireframe }) => { , GLTFLike, LoaderReturnType & ObjectMap, LoaderReturnType>' is not assignable to type 'BufferGeometry'. stl={stl} opacity={instances[idx].opacity} color={instances[idx].color} diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx index 7be5dfd..100e6b1 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/ThreeDViewer.tsx @@ -4,7 +4,7 @@ import { Suspense, useEffect, useMemo, useRef, useState } from "react"; import type * as THREE from "three"; import { useSelectedWorkspace } from "../../../hooks/useSelectedWorkspace.ts"; import { ViewerType, getNeuronUrlForDataset } from "../../../models/models.ts"; -import { type Dataset, OpenAPI } from "../../../rest"; +import type { Dataset } from "../../../rest"; import { CAMERA_FAR, CAMERA_FOV, @@ -61,12 +61,15 @@ function ThreeDViewer() { 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, - })); + return urls.map((url) => { + const neuronName = url.match(/([^/]+)(\.[^/]*)?$/)?.[1].replace(/(\.[^/]*)?$/, ""); + return { + id: `${neuronName}`, + url, + color: viewerData?.color || "#FFFFFF", + opacity: 1, + }; + }); }); setInstances(newInstances); diff --git a/applications/visualizer/frontend/src/contexts/GlobalContext.tsx b/applications/visualizer/frontend/src/contexts/GlobalContext.tsx index 524838b..7fc9417 100644 --- a/applications/visualizer/frontend/src/contexts/GlobalContext.tsx +++ b/applications/visualizer/frontend/src/contexts/GlobalContext.tsx @@ -6,7 +6,6 @@ import ErrorAlert from "../components/ErrorAlert.tsx"; import ErrorBoundary from "../components/ErrorBoundary.tsx"; import { ViewMode } from "../models"; import { Workspace } from "../models"; -import { GlobalError } from "../models/Error.ts"; import { type Dataset, DatasetsService } from "../rest"; import type { SerializedGlobalContext } from "./SerializedContext.tsx"; @@ -103,10 +102,10 @@ export const GlobalContextProvider: React.FC = ({ ch }; const handleErrors = (error: Error) => { - if (error instanceof GlobalError) { - setErrorMessage(error.message); - setOpenErrorAlert(true); - } + // if (error instanceof GlobalError) { + setErrorMessage(error.message); + setOpenErrorAlert(true); + // } }; const serializeGlobalContext = () => {