mirror of
https://github.com/wassname/optuna-dashboard.git
synced 2026-09-21 13:10:53 +08:00
Add 3d model view
This commit is contained in:
@@ -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 (
|
||||
<GizmoHelper alignment="bottom-right" margin={[80, 80]}>
|
||||
<GizmoViewport
|
||||
axisColors={["red", "green", "skyblue"]}
|
||||
labelColor="black"
|
||||
/>
|
||||
</GizmoHelper>
|
||||
)
|
||||
}
|
||||
|
||||
export function ModelViewer(props: ModelViewerProps): JSX.Element {
|
||||
const [geometry, setGeometry] = useState<THREE.BufferGeometry>()
|
||||
const [modelSize, setModelSize] = useState<THREE.Vector3>()
|
||||
|
||||
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 (
|
||||
<Canvas
|
||||
camera={cameraSettings}
|
||||
style={{ width: viewerWidth, height: props.height }}
|
||||
>
|
||||
<ambientLight />
|
||||
<OrbitControls />
|
||||
<gridHelper
|
||||
args={modelSize && [Math.max(modelSize?.x, modelSize?.y) * 5]}
|
||||
/>
|
||||
{props.hasGizmo && <CustomGizmoHelper />}
|
||||
<axesHelper />
|
||||
<pointLight position={[10, 10, 10]} />
|
||||
{geometry && (
|
||||
<mesh geometry={geometry}>
|
||||
<meshNormalMaterial />
|
||||
</mesh>
|
||||
)}
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
@@ -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 }) => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
} else if (a.filename.endsWith(".stl")) {
|
||||
return (
|
||||
<Card
|
||||
key={a.artifact_id}
|
||||
sx={{
|
||||
marginBottom: theme.spacing(2),
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: `${parseInt(width) * 2}px`,
|
||||
minHeight: "100%",
|
||||
margin: theme.spacing(0, 1, 1, 0),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<ModelViewer
|
||||
src={`/artifacts/${trial.study_id}/${trial.trial_id}/${a.artifact_id}`}
|
||||
alt={a.filename}
|
||||
width={width}
|
||||
height={height}
|
||||
hasGizmo={true}
|
||||
/>
|
||||
</Box>
|
||||
<CardContent
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
padding: `${theme.spacing(1)} !important`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
p: theme.spacing(0.5, 0),
|
||||
flexGrow: 1,
|
||||
wordWrap: "break-word",
|
||||
maxWidth: `calc(100% - ${theme.spacing(8)})`,
|
||||
}}
|
||||
>
|
||||
{a.filename}
|
||||
</Typography>
|
||||
<IconButton
|
||||
aria-label="delete artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
onClick={() => {
|
||||
openDeleteArtifactDialog(
|
||||
trial.study_id,
|
||||
trial.trial_id,
|
||||
a
|
||||
)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
aria-label="download artifact"
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ margin: "auto 0" }}
|
||||
download={a.filename}
|
||||
href={`/artifacts/${trial.study_id}/${trial.trial_id}/${a.artifact_id}`}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
} else if (a.mimetype.startsWith("audio")) {
|
||||
return (
|
||||
<Card
|
||||
|
||||
Reference in New Issue
Block a user