import * as THREE from 'three'; import { useCallback, useEffect, useRef, useState } from 'react'; import { retrieveGLTF, storeGLTF } from '../../../../../utils/indexDB/idbUtils'; import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; import { ThreeEvent, useFrame, useThree } from '@react-three/fiber'; import { useActiveTool, useDeletableFloorItem, useLimitDistance, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store'; import { AssetBoundingBox } from '../../functions/assetBoundingBox'; import { CameraControls, Html } from '@react-three/drei'; import useModuleStore, { useSubModuleStore } from '../../../../../store/useModuleStore'; import { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore'; import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore'; import { useProductContext } from '../../../../simulation/products/productContext'; import { useParams } from 'react-router-dom'; import { getUserData } from '../../../../../functions/getUserData'; import { useSceneContext } from '../../../../scene/sceneContext'; import { useVersionContext } from '../../../version/versionContext'; function Model({ asset }: { readonly asset: Asset }) { const { camera, controls, gl } = useThree(); const { activeTool } = useActiveTool(); const { toggleView } = useToggleView(); const { subModule } = useSubModuleStore(); const { activeModule } = useModuleStore(); const { assetStore, eventStore, productStore } = useSceneContext(); const { assets, removeAsset, setAnimations } = assetStore(); const { setTop } = useTopData(); const { setLeft } = useLeftData(); const { getIsEventInProduct } = productStore(); const { getEventByModelUuid } = eventStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset(); const { socket } = useSocketStore(); const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem(); const { setSelectedFloorItem } = useSelectedFloorItem(); const { limitDistance } = useLimitDistance(); const { renderDistance } = useRenderDistance(); const [isRendered, setIsRendered] = useState(false); const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; const [gltfScene, setGltfScene] = useState(null); const [boundingBox, setBoundingBox] = useState(null); const groupRef = useRef(null); const { toolMode } = useToolMode(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { projectId } = useParams(); const { userId, organization } = getUserData(); const [animationNames, setAnimationNames] = useState([]); const mixerRef = useRef(); const actions = useRef<{ [name: string]: THREE.AnimationAction }>({}); useEffect(() => { setDeletableFloorItem(null); }, [activeModule, toolMode]) useEffect(() => { const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); loader.setDRACOLoader(dracoLoader); const loadModel = async () => { try { // Check Cache // const assetId = asset.assetId; // const cachedModel = THREE.Cache.get(assetId); // if (cachedModel) { // setGltfScene(cachedModel.scene.clone()); // calculateBoundingBox(cachedModel.scene); // return; // } // Check Cache // const assetId = asset.assetId; // console.log('assetId: ', assetId); // const cachedModel = THREE.Cache.get(assetId); // console.log('cachedModel: ', cachedModel); // if (cachedModel) { // setGltfScene(cachedModel.scene.clone()); // calculateBoundingBox(cachedModel.scene); // return; // } const assetId = asset.assetId; const cachedModel = THREE.Cache.get(assetId); if (cachedModel) { const clonedScene = cachedModel.scene.clone(); clonedScene.animations = cachedModel.animations || []; setGltfScene(clonedScene); calculateBoundingBox(clonedScene); if (cachedModel.animations && clonedScene.animations.length > 0) { const animationName = clonedScene.animations.map((clip: any) => clip.name); setAnimationNames(animationName) setAnimations(asset.modelUuid, animationName) mixerRef.current = new THREE.AnimationMixer(clonedScene); clonedScene.animations.forEach((animation: any) => { const action = mixerRef.current!.clipAction(animation); actions.current[animation.name] = action; }); } return; } // Check IndexedDB const indexedDBModel = await retrieveGLTF(assetId); if (indexedDBModel) { const blobUrl = URL.createObjectURL(indexedDBModel); loader.load(blobUrl, (gltf) => { URL.revokeObjectURL(blobUrl); THREE.Cache.remove(blobUrl); THREE.Cache.add(assetId, gltf); setGltfScene(gltf.scene.clone()); calculateBoundingBox(gltf.scene); }, undefined, (error) => { echo.error(`[IndexedDB] Error loading ${asset.modelName}:`); URL.revokeObjectURL(blobUrl); } ); return; } // Fetch from Backend const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${assetId}`; const handleBackendLoad = async (gltf: GLTF) => { try { const response = await fetch(modelUrl); const modelBlob = await response.blob(); await storeGLTF(assetId, modelBlob); THREE.Cache.add(assetId, gltf); setGltfScene(gltf.scene.clone()); calculateBoundingBox(gltf.scene); } catch (error) { console.error(`[Backend] Error storing/loading ${asset.modelName}:`, error); } }; loader.load( modelUrl, handleBackendLoad, undefined, (error) => { echo.error(`[Backend] Error loading ${asset.modelName}:`); } ); } catch (err) { console.error("Failed to load model:", asset.assetId, err); } }; const calculateBoundingBox = (scene: THREE.Object3D) => { const box = new THREE.Box3().setFromObject(scene); setBoundingBox(box); }; loadModel(); }, []); useFrame(() => { const assetPosition = new THREE.Vector3(...asset.position); if (limitDistance) { if (!isRendered && assetPosition.distanceTo(camera.position) <= renderDistance) { setIsRendered(true); } else if (isRendered && assetPosition.distanceTo(camera.position) > renderDistance) { setIsRendered(false); } } else { if (!isRendered) { setIsRendered(true); } } }) const handleDblClick = (asset: Asset) => { if (asset) { if (activeTool === "cursor" && boundingBox && groupRef.current && activeModule === 'builder') { const size = boundingBox.getSize(new THREE.Vector3()); const center = boundingBox.getCenter(new THREE.Vector3()); const front = new THREE.Vector3(0, 0, 1); groupRef.current.localToWorld(front); front.sub(groupRef.current.position).normalize(); const distance = Math.max(size.x, size.y, size.z) * 2; const newPosition = center.clone().addScaledVector(front, distance); (controls as CameraControls).setPosition( newPosition.x, newPosition.y, newPosition.z, true ); (controls as CameraControls).setTarget(center.x, center.y, center.z, true); (controls as CameraControls).fitToBox(groupRef.current, true, { cover: true, paddingTop: 5, paddingLeft: 5, paddingBottom: 5, paddingRight: 5, }); setSelectedFloorItem(groupRef.current); } } }; const handleClick = (asset: Asset) => { if (activeTool === 'delete' && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) { //REST // const response = await deleteFloorItem(organization, asset.modelUuid, asset.modelName); //SOCKET const data = { organization, modelUuid: asset.modelUuid, modelName: asset.modelName, socketId: socket.id, userId, versionId: selectedVersion?.versionId || '', projectId } const response = socket.emit('v1:model-asset:delete', data) eventStore.getState().removeEvent(asset.modelUuid); productStore.getState().deleteEvent(asset.modelUuid); if (response) { removeAsset(asset.modelUuid); echo.success("Model Removed!"); } } } const handlePointerOver = useCallback((asset: Asset) => { if (activeTool === "delete" && activeModule === 'builder') { if (deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) { return; } else { setDeletableFloorItem(groupRef.current); } } }, [activeTool, activeModule, deletableFloorItem]); const handlePointerOut = useCallback((asset: Asset) => { if (activeTool === "delete" && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) { setDeletableFloorItem(null); } }, [activeTool, deletableFloorItem]); const handleContextMenu = (asset: Asset, evt: ThreeEvent) => { if (activeTool === "cursor" && subModule === 'simulations') { if (asset.modelUuid) { const canvasElement = gl.domElement; const isInProduct = getIsEventInProduct(selectedProduct.productUuid, asset.modelUuid); if (isInProduct) { const event = getEventByModelUuid(asset.modelUuid); if (event) { setSelectedAsset(event); const canvasRect = canvasElement.getBoundingClientRect(); const relativeX = evt.clientX - canvasRect.left; const relativeY = evt.clientY - canvasRect.top; setTop(relativeY); setLeft(relativeX); } else { clearSelectedAsset(); } } else { const event = getEventByModelUuid(asset.modelUuid); if (event) { setSelectedAsset(event) const canvasRect = canvasElement.getBoundingClientRect(); const relativeX = evt.clientX - canvasRect.left; const relativeY = evt.clientY - canvasRect.top; setTop(relativeY); setLeft(relativeX); } else { clearSelectedAsset() } } } else { clearSelectedAsset() } } else { clearSelectedAsset() } } useFrame((_, delta) => { if (mixerRef.current) { mixerRef.current.update(delta); } }); useEffect(() => { const handlePlay = (clipName: string) => { if (!mixerRef.current) return; Object.values(actions.current).forEach((action) => action.stop()); const action = actions.current[clipName]; if (action && asset.animationState?.playing) { action.reset().setLoop(THREE.LoopOnce, 1).play(); } }; handlePlay(asset.animationState?.current || ''); }, [asset]) return ( { if (!toggleView) { e.stopPropagation(); handleDblClick(asset); } }} onClick={(e) => { if (!toggleView) { e.stopPropagation(); handleClick(asset); } }} onPointerEnter={(e) => { if (!toggleView) { e.stopPropagation(); handlePointerOver(asset); } }} onPointerOut={(e) => { if (!toggleView) { e.stopPropagation(); handlePointerOut(asset); } }} onContextMenu={(e) => { e.stopPropagation(); handleContextMenu(asset, e); }} > {gltfScene && ( isRendered ? ( ) : ( ) )} {/*
{animationNames.map((name) => ( ))}
*/}
); } export default Model;