diff --git a/src/assets/components/model/model-detail-pane/ModelDetailPane.tsx b/src/assets/components/model/model-detail-pane/ModelDetailPane.tsx index 2548ff3..5548b3f 100644 --- a/src/assets/components/model/model-detail-pane/ModelDetailPane.tsx +++ b/src/assets/components/model/model-detail-pane/ModelDetailPane.tsx @@ -1,13 +1,8 @@ -import * as THREE from 'three' -import { Canvas, useLoader, useThree } from '@react-three/fiber' -import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js'; -import { Suspense, useContext, useLayoutEffect, useRef, useState } from "react"; +import { useContext, useEffect, useRef, useState } from "react"; import { Asset } from "../../../entities/Assets.ts"; -import { Center, GizmoHelper, GizmoViewport, Grid, Html, OrbitControls, useProgress } from '@react-three/drei' -import { useElementSize } from "@mantine/hooks"; -import { Alert, lighten } from "@mantine/core"; -import { SettingsContext } from '@/core/settings/settingsContext.ts'; - +import { Stack } from "@mantine/core"; +import { SettingsContext } from "@/core/settings/settingsContext.ts"; +import { Viewer3D, createViewer3D } from './Viewer3D.ts'; type ModelProps = { color: string, @@ -15,143 +10,42 @@ type ModelProps = { projectUuid: string } -function Model({ color, model, projectUuid }: ModelProps) { - const { settings } = useContext(SettingsContext); - const geom = useLoader(STLLoader, `${settings.localBackend}/projects/${projectUuid}/assets/${model.id}/file`); - const meshRef = useRef(null!) - - const [active, setActive] = useState(false) - - // Subscribe this component to the render-loop, rotate the mesh every frame - //useFrame((state, delta) => (meshRef.current.rotation.z += delta)) - return ( - <> - setActive(!active)} - ref={meshRef} - rotation={[-Math.PI / 2, 0, 0]} - scale={0.1}> - - - - - - ) -} - - -type SceneProps = { +type ModelDetailPaneProps = { models: Asset[], - projectUuid: string -} - -function Scene({ models, projectUuid }: SceneProps) { - const colors = ["#9d4b4b", "#4C5897", "#5474B4", "#504C97", "#6B31B2", "#C91A52"] - return ( - <> - - - - -
- }> - - {models.map((model, i) => ( - - ))} - - -
- - - - - - ) + projectUuid: string, + onClose: () => void } -function MoveCamera({ children, models }: { children: JSX.Element[], models: Asset[] }) { - const group = useRef() - const { camera } = useThree() - useLayoutEffect(() => { - if (!group.current) return; - const box = new THREE.Box3(); - box.setFromObject(group.current); - - - - const size = new THREE.Vector3(); - box.getSize(size); - const fov = camera.fov * (Math.PI / 180); - const fovh = 2 * Math.atan(Math.tan(fov / 2) * camera.aspect); - let dx = size.z / 2 + Math.abs(size.x / 2 / Math.tan(fovh / 2)); - let dy = size.z / 2 + Math.abs(size.y / 2 / Math.tan(fov / 2)); - let cameraZ = Math.max(dx, dy); - - // offset the camera, if desired (to avoid filling the whole canvas) - cameraZ *= 1.25; - - camera.position.set(0, 0, cameraZ); - - const newX = camera.position.x - (size.x / 2); - const newY = camera.position.y - (size.y / 2); - group.current.position.set(newX, newY, group.current.position.z) - - // set the far plane of the camera so that it easily encompasses the whole object - const minZ = box.min.z; - const cameraToFarEdge = (minZ < 0) ? -minZ + cameraZ : cameraZ - minZ; - +export function ModelDetailPane({ models, projectUuid, onClose }: ModelDetailPaneProps) { + const parent = useRef(); + const { settings } = useContext(SettingsContext); + const [viewer3D, setViewer3D] = useState(); + + useEffect(()=>{ + if(!parent.current) return; + if(!viewer3D) return; + viewer3D.setModels(models); + }, [models, viewer3D]); - const box3Helper = new THREE.Box3Helper(box, 0x00ff00); - box3Helper.material.linewidth = 3; - group.current.add(box3Helper); + useEffect(() => { + if(!parent.current) return; - const axesHelper = new THREE.AxesHelper(5); - const center = new THREE.Vector3(); - box.getCenter(center) - axesHelper.position.set(center.x, center.y, center.z) - group.current.add(axesHelper); + let viewer = createViewer3D(parent.current, settings.localBackend); + setViewer3D(viewer); - camera.far = cameraToFarEdge * 3; - camera.updateProjectionMatrix(); return () => { - if (group.current) { - group.current.remove(box3Helper); - group.current.remove(axesHelper); - } + viewer.destroy(); } - }, [models]); - return ( - - {children} - - ); -} - -function Progress() { - const { progress, loaded } = useProgress() - return {progress} % loaded {loaded} -} + }, []) -type ModelDetailPaneProps = { - models: Asset[], - projectUuid: string - onClose: () => void; -} -export function ModelDetailPane({ models, projectUuid, onClose }: ModelDetailPaneProps) { - console.log(models); - const { ref, width } = useElementSize(); return ( - - - - - + ); } @@ -168,5 +62,5 @@ function Ground() { followCamera: false, infiniteGrid: true } - return + return } \ No newline at end of file diff --git a/src/assets/components/model/model-detail-pane/Viewer3D.ts b/src/assets/components/model/model-detail-pane/Viewer3D.ts new file mode 100644 index 0000000..d4e1caa --- /dev/null +++ b/src/assets/components/model/model-detail-pane/Viewer3D.ts @@ -0,0 +1,171 @@ +import { Asset } from "@/assets/entities/Assets"; +import * as THREE from 'three'; +import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js"; +import { OrbitControls } from 'three/addons/controls/OrbitControls.js' +import {ViewHelper} from 'three/addons/helpers/ViewHelper.js' +import { Box } from "@mantine/core"; + +type State = { + models: Asset[] | null, + modelsRendered: ModelMesh[], + parent: HTMLElement, + scene: THREE.Scene, + camera: THREE.PerspectiveCamera, + renderer: THREE.WebGLRenderer + loader: STLLoader, + local_backend: string, + controls: OrbitControls | null, + group: THREE.Group, + dirty: boolean, + boxHelper: THREE.Box3Helper | null, +} + +type ModelMesh = { + id: String, + mesh: THREE.Mesh, + model: Asset +} + +export type Viewer3D = { + destroy(): void; + setModels(models: Asset[]): void; +} + +export const createViewer3D = (parent: HTMLElement, local_backend: string): Viewer3D => { + const state: State = { + models: [], + modelsRendered: [], + parent: parent, + camera: new THREE.PerspectiveCamera(20, parent.offsetWidth / parent.offsetHeight, 1, 1000), + scene: new THREE.Scene(), + local_backend: local_backend, + renderer: new THREE.WebGLRenderer({ antialias: true }), + loader: new STLLoader(), + controls: null, + group: new THREE.Group(), + dirty: false, + boxHelper: null + } + + state.scene = new THREE.Scene(); + state.scene.background = new THREE.Color( 0x333333 ); + + state.renderer.setPixelRatio( parent.offsetWidth / parent.offsetHeight ); + state.renderer.setSize( parent.offsetWidth , parent.offsetHeight ); + parent.appendChild( state.renderer.domElement ); + + // state.camera = new THREE.PerspectiveCamera( 60, parent.offsetWidth / parent.offsetHeight, 1, 1000 ); + state.camera.position.set( 400, 200, 0 ); + state.camera.lookAt( 0,0,0 ); + + // controls + + state.controls = new OrbitControls( state.camera, state.renderer.domElement ); + + state.controls.enableDamping = true; + state.controls.dampingFactor = 0.05; + state.controls.screenSpacePanning = false; + state.controls.minDistance = 100; + state.controls.maxDistance = 500; + state.controls.maxPolarAngle = Math.PI / 2; + + // lights + const dirLight1 = new THREE.DirectionalLight( 0xffffff, 3 ); + dirLight1.position.set( 1, 1, 1 ); + state.scene.add( dirLight1 ); + + const dirLight2 = new THREE.DirectionalLight( 0x002288, 3 ); + dirLight2.position.set( -1, -1, -1 ); + state.scene.add( dirLight2 ); + + const dirLight3 = new THREE.DirectionalLight( 0xffffff, 1 ); + dirLight3.position.set( -1, -1, -1 ); + state.scene.add( dirLight3 ); + + const ambientLight = new THREE.AmbientLight( 0x555555 ); + // state.scene.add( ambientLight ); + + state.scene.add(state.group) + + + function onParentResize() { + state.camera.aspect = parent.offsetWidth / parent.offsetHeight; + state.renderer.setSize(state.parent.offsetWidth, state.parent.offsetHeight); + state.camera.updateProjectionMatrix(); + } + + function drawHelpers(){ + state.scene.updateWorldMatrix(true, true); //make sure all transforms are updated after loading models + + const groupCenter = new THREE.Vector3(); + const box = new THREE.Box3(); + box.setFromObject(state.group); + box.getCenter(groupCenter); + + if(state.boxHelper){ + state.scene.remove(state.boxHelper); + } + state.boxHelper = new THREE.Box3Helper( box, 0x719CD6 ); + state.scene.add( state.boxHelper ); + + //set camera and controls to look to center of boundingbox + state.camera.lookAt(groupCenter); + state.controls?.target.set(groupCenter.x, groupCenter.y, groupCenter.z); + } + + function animate() { + if(state.dirty){ + drawHelpers(); + state.dirty = false; + } + state.renderer.clear(); + state.controls.update(); + + // helper.render(state.renderer); + state.renderer.render(state.scene, state.camera); + requestAnimationFrame(animate); + } + + window.addEventListener( 'resize', onParentResize ); + animate(); + + + return { + destroy() { + state.renderer.dispose(); + state.renderer.forceContextLoss(); + console.log("WebGL Context Destroyed"); + }, + setModels(models: Asset[]) { + state.models = models; + const material = new THREE.MeshPhongMaterial({ color: 0xd5d5d5, specular: 0x494949, shininess: 10, flatShading : true}); + state.group.clear(); + state.models.forEach((model) => { + const renderedModel = state.modelsRendered.find(mr => {return model.id == mr.id}); + if(renderedModel){ + state.group.add(renderedModel.mesh); + state.dirty = true; + return; + } + + state.loader.load(`${state.local_backend}/projects/${model.project_uuid}/assets/${model.id}/file`, function (geometry) { + + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.set(0, 0, 0); + mesh.rotation.set(-Math.PI / 2, 0, Math.PI / 2); + mesh.scale.set(1,1,1); + + mesh.castShadow = true; + mesh.receiveShadow = true; + mesh.geometry.computeBoundingBox(); + + state.group.add(mesh); + const id:String = model.id; + state.modelsRendered.push({mesh: mesh, id: id, model: model}); + state.dirty = true; + }); + }); + } + } +} \ No newline at end of file