add initial components and utility functions for simulation and builder modules

This commit is contained in:
2025-03-25 14:00:03 +05:30
parent 61b3c4ee2c
commit 2303682a15
164 changed files with 13967 additions and 52 deletions

View File

@@ -0,0 +1,186 @@
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import gsap from 'gsap';
import { toast } from 'react-toastify';
import TempLoader from './tempLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import * as Types from "../../../../types/world/worldTypes";
import { retrieveGLTF, storeGLTF } from '../../../../utils/indexDB/idbUtils';
// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi';
import { Socket } from 'socket.io-client';
import * as CONSTANTS from '../../../../types/world/worldConstants';
async function addAssetModel(
raycaster: THREE.Raycaster,
camera: THREE.Camera,
pointer: THREE.Vector2,
floorGroup: Types.RefGroup,
setFloorItems: Types.setFloorItemSetState,
itemsGroup: Types.RefGroup,
isTempLoader: Types.RefBoolean,
tempLoader: Types.RefMesh,
socket: Socket<any>,
selectedItem: any,
setSelectedItem: any,
plane: Types.RefMesh,
): Promise<void> {
////////// Load Floor GLtf's and set the positions, rotation, type etc. in state and store in localstorage //////////
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
try {
isTempLoader.current = true;
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);
raycaster.setFromCamera(pointer, camera);
const floorIntersections = raycaster.intersectObjects(floorGroup.current.children, true);
const intersectedFloor = floorIntersections.find(intersect => intersect.object.name.includes("Floor"));
const planeIntersections = raycaster.intersectObject(plane.current!, true);
const intersectedPlane = planeIntersections[0];
let intersectPoint: THREE.Vector3 | null = null;
if (intersectedFloor && intersectedPlane) {
intersectPoint = intersectedFloor.distance < intersectedPlane.distance ? (new THREE.Vector3(intersectedFloor.point.x, Math.round(intersectedFloor.point.y), intersectedFloor.point.z)) : (new THREE.Vector3(intersectedPlane.point.x, 0, intersectedPlane.point.z));
} else if (intersectedFloor) {
intersectPoint = new THREE.Vector3(intersectedFloor.point.x, Math.round(intersectedFloor.point.y), intersectedFloor.point.z);
} else if (intersectedPlane) {
intersectPoint = new THREE.Vector3(intersectedPlane.point.x, 0, intersectedPlane.point.z);
}
if (intersectPoint) {
if (intersectPoint.y < 0) {
intersectPoint = new THREE.Vector3(intersectPoint.x, 0, intersectPoint.z);
}
const cachedModel = THREE.Cache.get(selectedItem.id);
if (cachedModel) {
// console.log(`[Cache] Fetching ${selectedItem.name}`);
handleModelLoad(cachedModel, intersectPoint!, selectedItem, itemsGroup, tempLoader, isTempLoader, setFloorItems, socket);
return;
} else {
const cachedModelBlob = await retrieveGLTF(selectedItem.id);
if (cachedModelBlob) {
// console.log(`Added ${selectedItem.name} from indexDB`);
const blobUrl = URL.createObjectURL(cachedModelBlob);
loader.load(blobUrl, (gltf) => {
URL.revokeObjectURL(blobUrl);
THREE.Cache.remove(blobUrl);
THREE.Cache.add(selectedItem.id, gltf);
handleModelLoad(gltf, intersectPoint!, selectedItem, itemsGroup, tempLoader, isTempLoader, setFloorItems, socket);
},
() => {
TempLoader(intersectPoint!, isTempLoader, tempLoader, itemsGroup);
});
} else {
// console.log(`Added ${selectedItem.name} from Backend`);
loader.load(`${url_Backend_dwinzo}/api/v1/AssetFile/${selectedItem.id}`, async (gltf) => {
const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v1/AssetFile/${selectedItem.id}`).then((res) => res.blob());
await storeGLTF(selectedItem.id, modelBlob);
THREE.Cache.add(selectedItem.id, gltf);
await handleModelLoad(gltf, intersectPoint!, selectedItem, itemsGroup, tempLoader, isTempLoader, setFloorItems, socket);
},
() => {
TempLoader(intersectPoint!, isTempLoader, tempLoader, itemsGroup);
});
}
}
}
} catch (error) {
console.error('Error fetching asset model:', error);
} finally {
setSelectedItem({});
}
}
async function handleModelLoad(
gltf: any,
intersectPoint: THREE.Vector3,
selectedItem: any,
itemsGroup: Types.RefGroup,
tempLoader: Types.RefMesh,
isTempLoader: Types.RefBoolean,
setFloorItems: Types.setFloorItemSetState,
socket: Socket<any>
) {
const model = gltf.scene.clone();
model.userData = { name: selectedItem.name, modelId: selectedItem.id };
model.position.set(intersectPoint!.x, 3 + intersectPoint!.y, intersectPoint!.z);
model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap);
model.traverse((child: any) => {
if (child) {
child.castShadow = true;
child.receiveShadow = true;
}
});
itemsGroup.current.add(model);
if (tempLoader.current) {
(<any>tempLoader.current.material).dispose();
(<any>tempLoader.current.geometry).dispose();
itemsGroup.current.remove(tempLoader.current);
tempLoader.current = undefined;
}
const newFloorItem: Types.FloorItemType = {
modeluuid: model.uuid,
modelname: selectedItem.name,
modelfileID: selectedItem.id,
position: [intersectPoint!.x, intersectPoint!.y, intersectPoint!.z],
rotation: { x: model.rotation.x, y: model.rotation.y, z: model.rotation.z, },
isLocked: false,
isVisible: true
};
setFloorItems((prevItems) => {
const updatedItems = [...(prevItems || []), newFloorItem];
localStorage.setItem("FloorItems", JSON.stringify(updatedItems));
return updatedItems;
});
const email = localStorage.getItem("email");
const organization = email ? email.split("@")[1].split(".")[0] : "default";
//REST
// await setFloorItemApi(
// organization,
// newFloorItem.modeluuid,
// newFloorItem.modelname,
// newFloorItem.position,
// { "x": model.rotation.x, "y": model.rotation.y, "z": model.rotation.z },
// newFloorItem.modelfileID!,
// false,
// true,
// );
//SOCKET
const data = {
organization,
modeluuid: newFloorItem.modeluuid,
modelname: newFloorItem.modelname,
modelfileID: newFloorItem.modelfileID,
position: newFloorItem.position,
rotation: { x: model.rotation.x, y: model.rotation.y, z: model.rotation.z },
isLocked: false,
isVisible: true,
socketId: socket.id,
};
socket.emit("v1:FloorItems:set", data);
gsap.to(model.position, { y: newFloorItem.position[1], duration: 1.5, ease: "power2.out" });
gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: "power2.out", onComplete: () => { toast.success("Model Added!"); } });
}
export default addAssetModel;

View File

@@ -0,0 +1,153 @@
import * as THREE from "three";
import gsap from "gsap";
import * as Types from "../../../../types/world/worldTypes";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { initializeDB, retrieveGLTF, storeGLTF } from "../../../../utils/indexDB/idbUtils";
import * as CONSTANTS from '../../../../types/world/worldConstants';
import { toast } from 'react-toastify';
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
let currentTaskId = 0; // Track the active task
let activePromises = new Map<number, boolean>(); // Map to track task progress
export default async function assetManager(
data: any,
itemsGroup: Types.RefGroup,
loader: GLTFLoader,
) {
const taskId = ++currentTaskId; // Increment taskId for each call
activePromises.set(taskId, true); // Mark task as active
// console.log("Received message from worker:", data);
if (data.toRemove.length > 0) {
data.toRemove.forEach((uuid: string) => {
const item = itemsGroup.current.getObjectByProperty("uuid", uuid);
if (item) {
// Traverse and dispose of resources
// item.traverse((child: THREE.Object3D) => {
// if (child instanceof THREE.Mesh) {
// if (child.geometry) child.geometry.dispose();
// if (Array.isArray(child.material)) {
// child.material.forEach((material) => {
// if (material.map) material.map.dispose();
// material.dispose();
// });
// } else if (child.material) {
// if (child.material.map) child.material.map.dispose();
// child.material.dispose();
// }
// }
// });
// Remove the object from the scene
itemsGroup.current.remove(item);
}
});
}
if (data.toAdd.length > 0) {
await initializeDB();
for (const item of data.toAdd) {
if (!activePromises.get(taskId)) return; // Stop processing if task is canceled
await new Promise<void>(async (resolve) => {
const modelUrl = `${url_Backend_dwinzo}/api/v1/AssetFile/${item.modelfileID!}`;
// Check Three.js Cache
const cachedModel = THREE.Cache.get(item.modelfileID!);
if (cachedModel) {
// console.log(`[Cache] Fetching ${item.modelname}`);
processLoadedModel(cachedModel.scene.clone(), item, itemsGroup, resolve);
return;
}
// Check IndexedDB
const indexedDBModel = await retrieveGLTF(item.modelfileID!);
if (indexedDBModel) {
// console.log(`[IndexedDB] Fetching ${item.modelname}`);
const blobUrl = URL.createObjectURL(indexedDBModel);
loader.load(
blobUrl,
(gltf) => {
URL.revokeObjectURL(blobUrl);
THREE.Cache.remove(blobUrl);
THREE.Cache.add(item.modelfileID!, gltf); // Add to cache
processLoadedModel(gltf.scene.clone(), item, itemsGroup, resolve);
},
undefined,
(error) => {
toast.error(`[IndexedDB] Error loading ${item.modelname}:`);
resolve();
}
);
return;
}
// Fetch from Backend
// console.log(`[Backend] Fetching ${item.modelname}`);
loader.load(
modelUrl,
async (gltf) => {
const modelBlob = await fetch(modelUrl).then((res) => res.blob());
await storeGLTF(item.modelfileID!, modelBlob); // Store in IndexedDB
THREE.Cache.add(item.modelfileID!, gltf); // Add to cache
processLoadedModel(gltf.scene.clone(), item, itemsGroup, resolve);
},
undefined,
(error) => {
toast.error(`[Backend] Error loading ${item.modelname}:`);
resolve();
}
);
});
}
function processLoadedModel(
gltf: any,
item: Types.FloorItemType,
itemsGroup: Types.RefGroup,
resolve: () => void
) {
if (!activePromises.get(taskId)) return; // Stop processing if task is canceled
const existingModel = itemsGroup.current.getObjectByProperty("uuid", item.modeluuid);
if (existingModel) {
// console.log(`Model ${item.modelname} already exists in the scene.`);
resolve();
return;
}
const model = gltf;
model.uuid = item.modeluuid;
model.userData = { name: item.modelname, modelId: item.modelfileID };
model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap);
model.position.set(...item.position);
model.rotation.set(item.rotation.x, item.rotation.y, item.rotation.z);
model.traverse((child: any) => {
if (child.isMesh) {
// Clone the material to ensure changes are independent
// child.material = child.material.clone();
child.castShadow = true;
child.receiveShadow = true;
}
});
itemsGroup?.current?.add(model);
gsap.to(model.position, { y: item.position[1], duration: 1.5, ease: "power2.out" });
gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 0.5, ease: "power2.out", onStart: resolve, });
}
}
activePromises.delete(taskId); // Mark task as complete
}
// Cancel ongoing task when new call arrives
export function cancelOngoingTasks() {
activePromises.clear(); // Clear all ongoing tasks
}

View File

@@ -0,0 +1,25 @@
import * as Types from "../../../../types/world/worldTypes";
let lastUpdateTime = 0;
export default function assetVisibility(
itemsGroup: Types.RefGroup,
cameraPosition: Types.Vector3,
renderDistance: Types.Number,
throttleTime = 100
): void {
const now = performance.now();
if (now - lastUpdateTime < throttleTime) return;
lastUpdateTime = now;
if (!itemsGroup?.current || !cameraPosition) return;
itemsGroup.current.children.forEach((child) => {
const Distance = cameraPosition.distanceTo(child.position);
if (Distance <= renderDistance) {
child.visible = true;
} else {
child.visible = false;
}
});
}

View File

@@ -0,0 +1,43 @@
import * as THREE from 'three';
import * as Types from "../../../../types/world/worldTypes";
function DeletableHoveredFloorItems(
state: Types.ThreeState,
itemsGroup: Types.RefGroup,
hoveredDeletableFloorItem: Types.RefMesh,
setDeletableFloorItem: any
): void {
////////// Altering the color of the hovered GLTF item during the Deletion time //////////
state.raycaster.setFromCamera(state.pointer, state.camera);
const intersects = state.raycaster.intersectObjects(itemsGroup.current.children, true);
if (intersects.length > 0) {
if (intersects[0].object.name === "Pole") {
return;
}
if (hoveredDeletableFloorItem.current) {
hoveredDeletableFloorItem.current = undefined;
setDeletableFloorItem(null);
}
let currentObject = intersects[0].object;
while (currentObject) {
if (currentObject.name === "Scene") {
hoveredDeletableFloorItem.current = currentObject as THREE.Mesh;
setDeletableFloorItem(currentObject);
break;
}
currentObject = currentObject.parent as THREE.Object3D;
}
} else {
if (hoveredDeletableFloorItem.current) {
hoveredDeletableFloorItem.current = undefined;
setDeletableFloorItem(null);
}
}
}
export default DeletableHoveredFloorItems;

View File

@@ -0,0 +1,82 @@
import { toast } from 'react-toastify';
import * as THREE from 'three';
import * as Types from "../../../../types/world/worldTypes";
import { getFloorItems } from '../../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi';
// import { deleteFloorItem } from '../../../../services/factoryBuilder/assest/floorAsset/deleteFloorItemApi';
import { Socket } from 'socket.io-client';
async function DeleteFloorItems(
itemsGroup: Types.RefGroup,
hoveredDeletableFloorItem: Types.RefMesh,
setFloorItems: Types.setFloorItemSetState,
socket: Socket<any>
): Promise<void> {
////////// Deleting the hovered Floor GLTF from the scene (itemsGroup.current) and from the floorItems and also update it in the localstorage //////////
if (hoveredDeletableFloorItem.current) {
const email = localStorage.getItem('email')
const organization = (email!.split("@")[1]).split(".")[0];
const items = await getFloorItems(organization);
const removedItem = items.find(
(item: { modeluuid: string }) => item.modeluuid === hoveredDeletableFloorItem.current?.uuid
);
if (!removedItem) {
return
}
//REST
// const response = await deleteFloorItem(organization, removedItem.modeluuid, removedItem.modelname);
//SOCKET
const data = {
organization: organization,
modeluuid: removedItem.modeluuid,
modelname: removedItem.modelname,
socketId: socket.id
}
const response = socket.emit('v1:FloorItems:delete', data)
if (response) {
const updatedItems = items.filter(
(item: { modeluuid: string }) => item.modeluuid !== hoveredDeletableFloorItem.current?.uuid
);
const storedItems = JSON.parse(localStorage.getItem("FloorItems") || '[]');
const updatedStoredItems = storedItems.filter((item: { modeluuid: string }) => item.modeluuid !== hoveredDeletableFloorItem.current?.uuid);
localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems));
if (hoveredDeletableFloorItem.current) {
// Traverse and dispose of resources
hoveredDeletableFloorItem.current.traverse((child: THREE.Object3D) => {
if (child instanceof THREE.Mesh) {
if (child.geometry) child.geometry.dispose();
if (Array.isArray(child.material)) {
child.material.forEach((material) => {
if (material.map) material.map.dispose();
material.dispose();
});
} else if (child.material) {
if (child.material.map) child.material.map.dispose();
child.material.dispose();
}
}
});
// Remove the object from the scene
itemsGroup.current.remove(hoveredDeletableFloorItem.current);
}
setFloorItems(updatedItems);
toast.success("Model Removed!");
}
}
}
export default DeleteFloorItems;

View File

@@ -0,0 +1,29 @@
import * as THREE from 'three';
import * as Types from "../../../../types/world/worldTypes";
function TempLoader(
intersectPoint: Types.Vector3,
isTempLoader: Types.RefBoolean,
tempLoader: Types.RefMesh,
itemsGroup: Types.RefGroup
): void {
////////// Temporary Loader that indicates the gltf is being loaded //////////
////////// Bug: Can't Load More than one TempLoader if done, it won't leave the scene //////////
if (tempLoader.current) {
itemsGroup.current.remove(tempLoader.current);
}
if (isTempLoader.current) {
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: "white" });
tempLoader.current = new THREE.Mesh(cubeGeometry, cubeMaterial);
tempLoader.current.position.set(intersectPoint.x, 0.5 + intersectPoint.y, intersectPoint.z);
itemsGroup.current.add(tempLoader.current);
isTempLoader.current = false;
}
}
export default TempLoader;