Files
Dwinzo_Demo/app/src/modules/builder/asset/models/model/model.tsx

380 lines
15 KiB
TypeScript
Raw Normal View History

2025-06-10 15:28:23 +05:30
import * as THREE from 'three';
2025-06-12 09:31:51 +05:30
import { useCallback, useEffect, useRef, useState } from 'react';
2025-06-10 15:28:23 +05:30
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';
2025-06-10 15:28:23 +05:30
import { AssetBoundingBox } from '../../functions/assetBoundingBox';
import { CameraControls } from '@react-three/drei';
2025-06-10 15:28:23 +05:30
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';
2025-06-23 09:37:53 +05:30
import { getUserData } from '../../../../../functions/getUserData';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useVersionContext } from '../../../version/versionContext';
import { SkeletonUtils } from 'three-stdlib';
import { useAnimationPlaySpeed } from '../../../../../store/usePlayButtonStore';
2025-06-10 15:28:23 +05:30
function Model({ asset }: { readonly asset: Asset }) {
const { camera, controls, gl } = useThree();
const { activeTool } = useActiveTool();
const { toggleView } = useToggleView();
const { subModule } = useSubModuleStore();
const { activeModule } = useModuleStore();
const { speed } = useAnimationPlaySpeed();
2025-06-23 09:37:53 +05:30
const { assetStore, eventStore, productStore } = useSceneContext();
const { removeAsset, setAnimations, resetAnimation, setAnimationComplete, setCurrentAnimation: setAnmationAnimation } = assetStore();
2025-06-10 15:28:23 +05:30
const { setTop } = useTopData();
const { setLeft } = useLeftData();
2025-06-23 09:37:53 +05:30
const { getIsEventInProduct } = productStore();
const { getEventByModelUuid } = eventStore();
2025-06-10 15:28:23 +05:30
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
const { socket } = useSocketStore();
const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem();
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
const { limitDistance } = useLimitDistance();
2025-06-10 15:28:23 +05:30
const { renderDistance } = useRenderDistance();
const [isRendered, setIsRendered] = useState(false);
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
const [boundingBox, setBoundingBox] = useState<THREE.Box3 | null>(null);
const groupRef = useRef<THREE.Group>(null);
2025-06-12 09:31:51 +05:30
const { toolMode } = useToolMode();
2025-06-23 09:37:53 +05:30
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
2025-06-10 15:28:23 +05:30
const { projectId } = useParams();
2025-06-23 09:37:53 +05:30
const { userId, organization } = getUserData();
const mixerRef = useRef<THREE.AnimationMixer>();
const actions = useRef<{ [name: string]: THREE.AnimationAction }>({});
const [previousAnimation, setPreviousAnimation] = useState<string | null>(null);
const blendFactor = useRef(0);
const blendDuration = 0.5;
2025-06-10 15:28:23 +05:30
2025-06-12 09:31:51 +05:30
useEffect(() => {
setDeletableFloorItem(null);
if (selectedFloorItem === null) {
resetAnimation(asset.modelUuid);
}
}, [activeModule, toolMode, selectedFloorItem])
2025-06-12 09:31:51 +05:30
2025-06-10 15:28:23 +05:30
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
2025-06-10 15:28:23 +05:30
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;
});
}
2025-06-10 15:28:23 +05:30
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);
}
2025-06-10 15:28:23 +05:30
}
})
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) {
2025-06-23 09:37:53 +05:30
2025-06-10 15:28:23 +05:30
//REST
// const response = await deleteFloorItem(organization, asset.modelUuid, asset.modelName);
//SOCKET
const data = {
2025-06-23 09:37:53 +05:30
organization,
2025-06-10 15:28:23 +05:30
modelUuid: asset.modelUuid,
modelName: asset.modelName,
socketId: socket.id,
userId,
2025-06-23 09:37:53 +05:30
versionId: selectedVersion?.versionId || '',
2025-06-10 15:28:23 +05:30
projectId
}
const response = socket.emit('v1:model-asset:delete', data)
2025-06-23 09:37:53 +05:30
eventStore.getState().removeEvent(asset.modelUuid);
productStore.getState().deleteEvent(asset.modelUuid);
2025-06-10 15:28:23 +05:30
if (response) {
removeAsset(asset.modelUuid);
echo.success("Model Removed!");
}
}
}
2025-06-12 09:31:51 +05:30
const handlePointerOver = useCallback((asset: Asset) => {
if (activeTool === "delete" && activeModule === 'builder') {
2025-06-10 15:28:23 +05:30
if (deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
return;
} else {
setDeletableFloorItem(groupRef.current);
}
}
2025-06-12 09:31:51 +05:30
}, [activeTool, activeModule, deletableFloorItem]);
2025-06-10 15:28:23 +05:30
2025-06-12 09:31:51 +05:30
const handlePointerOut = useCallback((asset: Asset) => {
2025-06-10 15:28:23 +05:30
if (activeTool === "delete" && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
setDeletableFloorItem(null);
}
2025-06-12 09:31:51 +05:30
}, [activeTool, deletableFloorItem]);
2025-06-10 15:28:23 +05:30
const handleContextMenu = (asset: Asset, evt: ThreeEvent<MouseEvent>) => {
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 * speed);
}
});
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) {
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?.isPlaying]);
2025-06-10 15:28:23 +05:30
return (
<group
2025-06-12 09:31:51 +05:30
key={asset.modelUuid}
2025-06-10 15:28:23 +05:30
name='Asset Model'
ref={groupRef}
uuid={asset.modelUuid}
position={asset.position}
rotation={asset.rotation}
visible={asset.isVisible}
userData={asset}
onDoubleClick={(e) => {
if (!toggleView) {
e.stopPropagation();
handleDblClick(asset);
}
}}
onClick={(e) => {
if (!toggleView) {
e.stopPropagation();
handleClick(asset);
}
}}
2025-06-12 09:31:51 +05:30
onPointerEnter={(e) => {
2025-06-10 15:28:23 +05:30
if (!toggleView) {
e.stopPropagation();
handlePointerOver(asset);
}
}}
onPointerOut={(e) => {
if (!toggleView) {
e.stopPropagation();
handlePointerOut(asset);
}
}}
onContextMenu={(e) => {
e.stopPropagation();
handleContextMenu(asset, e);
}}
>
{gltfScene && (
isRendered ? (
<primitive object={gltfScene} />
) : (
<AssetBoundingBox boundingBox={boundingBox} />
)
)}
</group >
2025-06-10 15:28:23 +05:30
);
}
export default Model;