first commit
This commit is contained in:
297
app/src/modules/builder/asset/models/model/model.tsx
Normal file
297
app/src/modules/builder/asset/models/model/model.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import * as THREE from 'three';
|
||||
import { 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, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView } from '../../../../../store/builder/store';
|
||||
import { AssetBoundingBox } from '../../functions/assetBoundingBox';
|
||||
import { CameraControls } from '@react-three/drei';
|
||||
import { useAssetsStore } from '../../../../../store/builder/useAssetStore';
|
||||
import { useEventsStore } from "../../../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../../../store/simulation/useProductStore";
|
||||
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';
|
||||
|
||||
function Model({ asset }: { readonly asset: Asset }) {
|
||||
const { camera, controls, gl } = useThree();
|
||||
const { activeTool } = useActiveTool();
|
||||
const { toggleView } = useToggleView();
|
||||
const { subModule } = useSubModuleStore();
|
||||
const { activeModule } = useModuleStore();
|
||||
const { removeAsset } = useAssetsStore();
|
||||
const { setTop } = useTopData();
|
||||
const { setLeft } = useLeftData();
|
||||
const { getIsEventInProduct } = useProductStore();
|
||||
const { getEventByModelUuid } = useEventsStore();
|
||||
const { selectedProductStore } = useProductContext();
|
||||
const { selectedProduct } = selectedProductStore();
|
||||
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
|
||||
const { socket } = useSocketStore();
|
||||
const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem();
|
||||
const { setSelectedFloorItem } = useSelectedFloorItem();
|
||||
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);
|
||||
const { projectId } = useParams();
|
||||
|
||||
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 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 (!isRendered && assetPosition.distanceTo(camera.position) <= renderDistance) {
|
||||
setIsRendered(true);
|
||||
} else if (isRendered && assetPosition.distanceTo(camera.position) > renderDistance) {
|
||||
setIsRendered(false);
|
||||
}
|
||||
})
|
||||
|
||||
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) {
|
||||
const email = localStorage.getItem('email')
|
||||
const organization = (email!.split("@")[1]).split(".")[0];
|
||||
const userId = localStorage.getItem("userId");
|
||||
|
||||
//REST
|
||||
|
||||
// const response = await deleteFloorItem(organization, asset.modelUuid, asset.modelName);
|
||||
|
||||
//SOCKET
|
||||
|
||||
const data = {
|
||||
organization: organization,
|
||||
modelUuid: asset.modelUuid,
|
||||
modelName: asset.modelName,
|
||||
socketId: socket.id,
|
||||
userId,
|
||||
projectId
|
||||
}
|
||||
|
||||
const response = socket.emit('v1:model-asset:delete', data)
|
||||
|
||||
useEventsStore.getState().removeEvent(asset.modelUuid);
|
||||
useProductStore.getState().deleteEvent(asset.modelUuid);
|
||||
|
||||
if (response) {
|
||||
|
||||
removeAsset(asset.modelUuid);
|
||||
|
||||
echo.success("Model Removed!");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerOver = (asset: Asset) => {
|
||||
if (activeTool === "delete") {
|
||||
if (deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
|
||||
return;
|
||||
} else {
|
||||
setDeletableFloorItem(groupRef.current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerOut = (asset: Asset) => {
|
||||
if (activeTool === "delete" && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
|
||||
setDeletableFloorItem(null);
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<group
|
||||
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);
|
||||
}
|
||||
}}
|
||||
onPointerOver={(e) => {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default Model;
|
||||
Reference in New Issue
Block a user