Add 3d model view

This commit is contained in:
hrntsm
2023-08-07 11:29:07 +09:00
parent b9d6feb9da
commit 3472f353ed
2 changed files with 156 additions and 0 deletions
@@ -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