diff --git a/optuna_dashboard/ts/components/ModelViewer.tsx b/optuna_dashboard/ts/components/ModelViewer.tsx new file mode 100644 index 00000000..db5caee2 --- /dev/null +++ b/optuna_dashboard/ts/components/ModelViewer.tsx @@ -0,0 +1,81 @@ +import * as THREE from "three" +import React, { useState } from "react" +import { Canvas } from "@react-three/fiber" +import { GizmoHelper, GizmoViewport, OrbitControls } from "@react-three/drei" +import { STLLoader } from "three/examples/jsm/loaders/STLLoader" +import { PerspectiveCamera } from "three" + +interface ModelViewerProps { + src: string + alt: string + width: string + height: string + hasGizmo: boolean +} + +function CustomGizmoHelper(): JSX.Element { + return ( + + + + ) +} + +export function ModelViewer(props: ModelViewerProps): JSX.Element { + const [geometry, setGeometry] = useState() + const [modelSize, setModelSize] = useState() + + React.useEffect(() => { + const loader = new STLLoader() + loader.load(props.src, (geometry: THREE.BufferGeometry) => { + if (geometry) { + setGeometry(geometry) + geometry.computeBoundingBox() + if (geometry.boundingBox === null) { + setModelSize(new THREE.Vector3(10, 10, 10)) + } else { + const size = geometry.boundingBox.getSize(new THREE.Vector3()) + setModelSize(size) + } + } + }) + }, []) + const cameraPosition = modelSize + ? [modelSize.x * 1.5, modelSize.y * 1.5, modelSize.z * 1.5] + : [10, 10, 10] + const cameraSettings: PerspectiveCamera = { + fov: modelSize + ? Math.min(45, Math.atan(modelSize.y / modelSize.z) * (180 / Math.PI) * 2) + : 45, + aspect: window.innerWidth / window.innerHeight, + near: 0.1, + far: 1000, + position: cameraPosition, + } + + const viewerWidth = `${parseInt(props.width) * 2}px` + + return ( + + + + + {props.hasGizmo && } + + + {geometry && ( + + + + )} + + ) +} diff --git a/optuna_dashboard/ts/components/TrialList.tsx b/optuna_dashboard/ts/components/TrialList.tsx index c2c75dd3..165fe9d0 100644 --- a/optuna_dashboard/ts/components/TrialList.tsx +++ b/optuna_dashboard/ts/components/TrialList.tsx @@ -45,6 +45,7 @@ import { artifactIsAvailable } from "../state" import { actionCreator } from "../action" import { useDeleteArtifactDialog } from "./DeleteArtifactDialog" import { TrialFormWidgets } from "./TrialFormWidgets" +import { ModelViewer } from "./ModelViewer" const states: TrialState[] = [ "Complete", @@ -437,6 +438,80 @@ const TrialArtifact: FC<{ trial: Trial }> = ({ trial }) => { ) + } else if (a.filename.endsWith(".stl")) { + return ( + + + + + + + {a.filename} + + { + openDeleteArtifactDialog( + trial.study_id, + trial.trial_id, + a + ) + }} + > + + + + + + + + ) } else if (a.mimetype.startsWith("audio")) { return (