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, useSelectedAssets, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store'; import { AssetBoundingBox } from '../../functions/assetBoundingBox'; import { CameraControls } 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'; import { SkeletonUtils } from 'three-stdlib'; import { useAnimationPlaySpeed, usePauseButtonStore } from '../../../../../store/usePlayButtonStore'; import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi'; import { getAssetIksApi } from '../../../../../services/simulation/ik/getAssetIKs'; function Model({ asset }: { readonly asset: Asset }) { const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; const savedTheme: string = localStorage.getItem("theme") || "light"; const { camera, controls, gl } = useThree(); const { activeTool } = useActiveTool(); const { toolMode } = useToolMode(); const { toggleView } = useToggleView(); const { subModule } = useSubModuleStore(); const { activeModule } = useModuleStore(); const { speed } = useAnimationPlaySpeed(); const { isPaused } = usePauseButtonStore(); const { assetStore, eventStore, productStore } = useSceneContext(); const { removeAsset, setAnimations, resetAnimation, setAnimationComplete } = assetStore(); const { setTop } = useTopData(); const { setLeft } = useLeftData(); const { getIsEventInProduct, addPoint } = productStore(); const { getEventByModelUuid } = eventStore(); const { selectedProductStore } = useProductContext(); const { selectedProduct } = selectedProductStore(); const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset(); const { socket } = useSocketStore(); const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem(); const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem(); const { limitDistance } = useLimitDistance(); const { renderDistance } = useRenderDistance(); const leftDrag = useRef(false); const isLeftMouseDown = useRef(false); const rightDrag = useRef(false); const isRightMouseDown = useRef(false); const [isRendered, setIsRendered] = useState(false); const [gltfScene, setGltfScene] = useState(null); const [boundingBox, setBoundingBox] = useState(null); const [isSelected, setIsSelected] = useState(false); const groupRef = useRef(null); const mixerRef = useRef(); const actions = useRef<{ [name: string]: THREE.AnimationAction }>({}); const [previousAnimation, setPreviousAnimation] = useState(null); const [ikData, setIkData] = useState(); const blendFactor = useRef(0); const blendDuration = 0.5; const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { userId, organization } = getUserData(); const { projectId } = useParams(); const { selectedAssets } = useSelectedAssets(); const updateBackend = ( productName: string, productUuid: string, projectId: string, eventData: EventsSchema ) => { upsertProductOrEventApi({ productName: productName, productUuid: productUuid, projectId: projectId, eventDatas: eventData, versionId: selectedVersion?.versionId || '', }); }; useEffect(() => { if (!ikData && asset.eventData && asset.eventData.type === 'ArmBot') { getAssetIksApi(asset.assetId).then((data) => { if (data.iks) { const iks: IK[] = data.iks; setIkData(iks); } }) } }, [asset.modelUuid, ikData]) useEffect(() => { if (gltfScene) { gltfScene.traverse((child: any) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }) } }, [gltfScene]); useEffect(() => { setDeletableFloorItem(null); if (selectedFloorItem === null || selectedFloorItem.userData.modelUuid !== asset.modelUuid) { resetAnimation(asset.modelUuid); } }, [activeModule, toolMode, selectedFloorItem]) useEffect(() => { if (selectedFloorItem && selectedFloorItem.userData.modelUuid === asset.modelUuid) { setSelectedFloorItem(groupRef.current); } }, [isRendered, selectedFloorItem]) 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) { const clone: any = SkeletonUtils.clone(cachedModel.scene); clone.animations = cachedModel.animations || []; setGltfScene(clone); calculateBoundingBox(clone); if (cachedModel.animations && clone.animations.length > 0) { const animationName = clone.animations.map((clip: any) => clip.name); setAnimations(asset.modelUuid, animationName) mixerRef.current = new THREE.AnimationMixer(clone); clone.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 = (evt: ThreeEvent, asset: Asset) => { if (leftDrag.current || toggleView) return; 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); const updatedEvents = productStore.getState().deleteEvent(asset.modelUuid); updatedEvents.forEach((event) => { updateBackend( selectedProduct.productName, selectedProduct.productUuid, projectId || '', event ); }) if (response) { removeAsset(asset.modelUuid); echo.success("Model Removed!"); } } else if (activeModule === 'simulation' && subModule === "simulations" && activeTool === 'pen') { if (asset.eventData && asset.eventData.type === 'Conveyor') { const intersectedPoint = evt.point; const localPosition = groupRef.current?.worldToLocal(intersectedPoint.clone()); if (localPosition) { const conveyorPoint: ConveyorPointSchema = { uuid: THREE.MathUtils.generateUUID(), position: [localPosition?.x, localPosition?.y, localPosition?.z], rotation: [0, 0, 0], action: { actionUuid: THREE.MathUtils.generateUUID(), actionName: `Action 1`, actionType: 'default', material: 'Default Material', delay: 0, spawnInterval: 5, spawnCount: 1, triggers: [] } } const event = addPoint(selectedProduct.productUuid, asset.modelUuid, conveyorPoint); if (event) { updateBackend( selectedProduct.productName, selectedProduct.productUuid, projectId || '', event ); } } } } } 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((evt: ThreeEvent, asset: Asset) => { if (evt.intersections.length === 0 && activeTool === "delete" && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) { setDeletableFloorItem(null); } }, [activeTool, deletableFloorItem]); const handleContextMenu = (asset: Asset, evt: ThreeEvent) => { if (rightDrag.current || toggleView) return; 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() } } const handleAnimationComplete = useCallback(() => { if (asset.animationState) { setAnimationComplete(asset.modelUuid, true); } }, [asset.animationState]); useFrame((_, delta) => { if (mixerRef.current) { mixerRef.current.update(delta * (activeModule === 'simulation' ? speed : 1)); } }); useEffect(() => { if (!asset.animationState || !mixerRef.current) return; const { current, loopAnimation, isPlaying } = asset.animationState; const currentAction = actions.current[current]; const previousAction = previousAnimation ? actions.current[previousAnimation] : null; if (isPlaying && currentAction && activeModule === 'simulation' && !isPaused) { blendFactor.current = 0; currentAction.reset(); currentAction.setLoop(loopAnimation ? THREE.LoopRepeat : THREE.LoopOnce, loopAnimation ? Infinity : 1); currentAction.clampWhenFinished = true; if (previousAction && previousAction !== currentAction) { previousAction.crossFadeTo(currentAction, blendDuration, false); } currentAction.play(); mixerRef.current.addEventListener('finished', handleAnimationComplete); setPreviousAnimation(current); } else { Object.values(actions.current).forEach((action) => action.stop()); } return () => { if (mixerRef.current) { mixerRef.current.removeEventListener('finished', handleAnimationComplete); } }; }, [asset.animationState?.current, asset.animationState?.isCompleted, asset.animationState?.isPlaying, isPaused, activeModule]); useEffect(() => { const canvasElement = gl.domElement; const onPointerDown = (evt: any) => { if (evt.button === 0) { isLeftMouseDown.current = true; leftDrag.current = false; } if (evt.button === 2) { isRightMouseDown.current = true; rightDrag.current = false; } }; const onPointerMove = () => { if (isLeftMouseDown.current) { leftDrag.current = true; } if (isRightMouseDown.current) { rightDrag.current = true; } }; const onPointerUp = (evt: any) => { if (evt.button === 0) { isLeftMouseDown.current = false; } if (evt.button === 2) { isRightMouseDown.current = false; } }; canvasElement.addEventListener('pointerdown', onPointerDown); canvasElement.addEventListener('pointermove', onPointerMove); canvasElement.addEventListener('pointerup', onPointerUp); return () => { canvasElement.removeEventListener('pointerdown', onPointerDown); canvasElement.removeEventListener('pointermove', onPointerMove); canvasElement.removeEventListener('pointerup', onPointerUp); } }, [gl]) useEffect(() => { if (selectedAssets.length > 0) { if (selectedAssets.some((selectedAsset: THREE.Object3D) => selectedAsset.userData.modelUuid === asset.modelUuid)) { setIsSelected(true); } else { setIsSelected(false); } } else { setIsSelected(false); } }, [selectedAssets]) return ( { e.stopPropagation(); if (!toggleView) { handleDblClick(asset); } }} onClick={(e) => { e.stopPropagation(); if (!toggleView) { handleClick(e, asset); } }} onPointerOver={(e) => { e.stopPropagation(); if (!toggleView) { handlePointerOver(asset); } }} onPointerLeave={(e) => { e.stopPropagation(); if (!toggleView) { handlePointerOut(e, asset); } }} onContextMenu={(e) => { e.stopPropagation(); handleContextMenu(asset, e); }} > {gltfScene && ( <> {isRendered ? ( ) : ( )} {isSelected && } )} ); } export default Model;