diff --git a/optuna_dashboard/ts/components/ModelViewer.tsx b/optuna_dashboard/ts/components/ModelViewer.tsx index 9695f139..5f0d00bc 100644 --- a/optuna_dashboard/ts/components/ModelViewer.tsx +++ b/optuna_dashboard/ts/components/ModelViewer.tsx @@ -3,6 +3,7 @@ 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 { Rhino3dmLoader } from "three/examples/jsm/loaders/3DMLoader" import { PerspectiveCamera } from "three" interface ModelViewerProps { @@ -10,6 +11,7 @@ interface ModelViewerProps { width: string height: string hasGizmo: boolean + filetype: string | undefined } function CustomGizmoHelper(): JSX.Element { @@ -24,23 +26,46 @@ function CustomGizmoHelper(): JSX.Element { } export function ModelViewer(props: ModelViewerProps): JSX.Element { - const [geometry, setGeometry] = useState() + 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) + if ("stl" === props.filetype) { + const stlLoader = new STLLoader() + stlLoader.load(props.src, (stlMesh: THREE.BufferGeometry) => { + if (stlMesh) { + setGeometry([stlMesh]) + stlMesh.computeBoundingBox() + if (stlMesh.boundingBox === null) { + setModelSize(new THREE.Vector3(10, 10, 10)) + } else { + const size = stlMesh.boundingBox.getSize(new THREE.Vector3()) + setModelSize(size) + } } - } - }) + }) + } else if ("3dm" === props.filetype) { + const loader = new Rhino3dmLoader() + loader.setLibraryPath("https://cdn.jsdelivr.net/npm/rhino3dm@7.15.0/") + loader.load(props.src, (object: THREE.Object3D) => { + object.traverse(function (child) { + // rotate to y-up + child.rotateX(-Math.PI / 4) + }) + const meshes = object.children as THREE.Mesh[] + const rhinoMeshes = meshes.map((mesh) => mesh.geometry) + if (rhinoMeshes.length > 0) { + setGeometry(rhinoMeshes) + rhinoMeshes[0].computeBoundingBox() + if (rhinoMeshes[0].boundingBox === null) { + setModelSize(new THREE.Vector3(10, 10, 10)) + } else { + const size = rhinoMeshes[0].boundingBox.getSize(new THREE.Vector3()) + setModelSize(size) + } + } + }) + } }, []) const cameraPosition = modelSize ? [modelSize.x * 1.5, modelSize.y * 1.5, modelSize.z * 1.5] @@ -51,8 +76,8 @@ export function ModelViewer(props: ModelViewerProps): JSX.Element { : 45, aspect: window.innerWidth / window.innerHeight, near: 0.1, + position: new THREE.Vector3(...cameraPosition), far: 1000, - position: cameraPosition, } return ( @@ -67,11 +92,12 @@ export function ModelViewer(props: ModelViewerProps): JSX.Element { /> {props.hasGizmo && } - {geometry && ( - - - - )} + {geometry.length > 0 && + geometry.map((geo, index) => ( + + + + ))} ) } diff --git a/optuna_dashboard/ts/components/TrialList.tsx b/optuna_dashboard/ts/components/TrialList.tsx index 744db8d4..05a7bb2a 100644 --- a/optuna_dashboard/ts/components/TrialList.tsx +++ b/optuna_dashboard/ts/components/TrialList.tsx @@ -330,7 +330,9 @@ const TrialArtifact: FC<{ trial: Trial }> = ({ trial }) => { const [openDeleteArtifactDialog, renderDeleteArtifactDialog] = useDeleteArtifactDialog() const [dragOver, setDragOver] = useState(false) - const [open3dModelViewer, setOpen3dModelViewer] = useState(false) + const [open3dModelViewer, setOpen3dModelViewer] = useState<{ + [key: string]: boolean + }>({}) const width = "200px" const height = "150px" @@ -442,7 +444,10 @@ const TrialArtifact: FC<{ trial: Trial }> = ({ trial }) => { ) - } else if (a.filename.endsWith(".stl")) { + } else if ( + a.filename.endsWith(".stl") || + a.filename.endsWith(".3dm") + ) { return ( = ({ trial }) => { width={width} height={height} hasGizmo={false} + filetype={a.filename.split(".").pop()} /> = ({ trial }) => { {a.filename} { - setOpen3dModelViewer(true) + setOpen3dModelViewer(() => { + const obj = { ...open3dModelViewer } + obj[a.filename] = true + return obj + }) }} > { - setOpen3dModelViewer(false) + setOpen3dModelViewer(() => { + const obj = { ...open3dModelViewer } + obj[a.filename] = false + return obj + }) }} > = ({ trial }) => { width={`${innerWidth * 0.8}px`} height={`${innerHeight * 0.8}px`} hasGizmo={true} + filetype={a.filename.split(".").pop()} />