diff --git a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts index 0d96c8d..096665a 100644 --- a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts +++ b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts @@ -3,7 +3,6 @@ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; import gsap from 'gsap'; import * as THREE from 'three'; import * as CONSTANTS from '../../../types/world/worldConstants'; -import { toast } from 'react-toastify'; import * as Types from "../../../types/world/worldTypes"; import { initializeDB, retrieveGLTF, storeGLTF } from '../../../utils/indexDB/idbUtils'; import { getCamera } from '../../../services/factoryBuilder/camera/getCameraApi'; @@ -91,7 +90,7 @@ async function loadInitialFloorItems( }, undefined, (error) => { - toast.error(`[IndexedDB] Error loading ${item.modelName}:`); + echo.error(`[IndexedDB] Error loading ${item.modelName}:`); URL.revokeObjectURL(blobUrl); resolve(); } @@ -111,7 +110,7 @@ async function loadInitialFloorItems( }, undefined, (error) => { - toast.error(`[Backend] Error loading ${item.modelName}:`); + echo.error(`[Backend] Error loading ${item.modelName}:`); resolve(); } ); diff --git a/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts b/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts index 2258f3b..96395ba 100644 --- a/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts +++ b/app/src/modules/builder/IntialLoad/loadInitialWallItems.ts @@ -33,7 +33,7 @@ async function loadInitialWallItems( const loadedWallItems = await Promise.all(items.map(async (item: Types.WallItem) => { // Check THREE.js cache first - const cachedModel = THREE.Cache.get(item.modelName!); + const cachedModel = THREE.Cache.get(item.modelfileID!); if (cachedModel) { return processModel(cachedModel, item); } @@ -45,7 +45,7 @@ async function loadInitialWallItems( return new Promise((resolve) => { loader.load(blobUrl, (gltf) => { URL.revokeObjectURL(blobUrl); - THREE.Cache.add(item.modelName!, gltf); + THREE.Cache.add(item.modelfileID!, gltf); resolve(processModel(gltf, item)); }); }); @@ -58,8 +58,8 @@ async function loadInitialWallItems( try { // Cache the model const modelBlob = await fetch(modelUrl).then((res) => res.blob()); - await storeGLTF(item.modelName!, modelBlob); - THREE.Cache.add(item.modelName!, gltf); + await storeGLTF(item.modelfileID!, modelBlob); + THREE.Cache.add(item.modelfileID!, gltf); resolve(processModel(gltf, item)); } catch (error) { console.error('Failed to cache model:', error); diff --git a/app/src/modules/builder/assetGroup/assetsGroup.tsx b/app/src/modules/builder/assetGroup/assetsGroup.tsx new file mode 100644 index 0000000..9e57fb7 --- /dev/null +++ b/app/src/modules/builder/assetGroup/assetsGroup.tsx @@ -0,0 +1,125 @@ +import * as THREE from "three" +import { useEffect } from 'react' +import { getFloorAssets } from '../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi'; +import { useLoadingProgress } from '../../../store/builder/store'; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; +import { FloorItems } from "../../../types/world/worldTypes"; +import { useAssetsStore } from "../../../store/builder/useAssetStore"; +import Models from "./models/models"; +import { useGLTF } from "@react-three/drei"; + +const gltfLoaderWorker = new Worker( + new URL( + "../../../services/factoryBuilder/webWorkers/gltfLoaderWorker.js", + import.meta.url + ) +); + +function AssetsGroup() { + const { setLoadingProgress } = useLoadingProgress(); + const { setAssets } = useAssetsStore(); + + 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); + + useEffect(() => { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + + let totalAssets = 0; + let loadedAssets = 0; + + const updateLoadingProgress = (progress: number) => { + if (progress < 100) { + setLoadingProgress(progress); + } else if (progress === 100) { + setTimeout(() => { + setLoadingProgress(100); + setTimeout(() => { + setLoadingProgress(0); + }, 1500); + }, 1000); + } + }; + + getFloorAssets(organization).then((data) => { + if (data.length > 0) { + const uniqueItems = (data as FloorItems).filter((item, index, self) => index === self.findIndex((t) => t.modelfileID === item.modelfileID)); + totalAssets = uniqueItems.length; + if (totalAssets === 0) { + updateLoadingProgress(100); + return; + } + gltfLoaderWorker.postMessage({ floorItems: uniqueItems }); + } else { + gltfLoaderWorker.postMessage({ floorItems: [] }); + updateLoadingProgress(100); + } + }); + + gltfLoaderWorker.onmessage = async (event) => { + if (event.data.message === "gltfLoaded" && event.data.modelBlob) { + const blobUrl = URL.createObjectURL(event.data.modelBlob); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(event.data.modelID, gltf); + + loadedAssets++; + const progress = Math.round((loadedAssets / totalAssets) * 100); + updateLoadingProgress(progress); + + if (loadedAssets === totalAssets) { + const assets: Asset[] = []; + getFloorAssets(organization).then((data: FloorItems) => { + data.forEach((item) => { + if (item.eventData) { + assets.push({ + modelUuid: item.modelUuid, + modelName: item.modelName, + assetId: item.modelfileID, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + isLocked: item.isLocked, + isCollidable: false, + isVisible: item.isVisible, + opacity: 1, + eventData: item.eventData + }) + } else { + assets.push({ + modelUuid: item.modelUuid, + modelName: item.modelName, + assetId: item.modelfileID, + position: item.position, + rotation: [item.rotation.x, item.rotation.y, item.rotation.z], + isLocked: item.isLocked, + isCollidable: false, + isVisible: item.isVisible, + opacity: 1, + }) + } + }) + setAssets(assets); + }) + updateLoadingProgress(100); + } + }); + } + }; + }, []); + + return ( + <> + + + ) +} + +export default AssetsGroup; \ No newline at end of file diff --git a/app/src/modules/builder/assetGroup/models/model/model.tsx b/app/src/modules/builder/assetGroup/models/model/model.tsx new file mode 100644 index 0000000..55eb1a6 --- /dev/null +++ b/app/src/modules/builder/assetGroup/models/model/model.tsx @@ -0,0 +1,82 @@ +import { Outlines } from '@react-three/drei'; +import { useEffect, 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 * as THREE from 'three'; + +function Model({ asset }: { asset: Asset }) { + const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + const [gltfScene, setGltfScene] = useState(null); + + 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 { + const cachedModel = THREE.Cache.get(asset.assetId!); + if (cachedModel) { + setGltfScene(cachedModel); + return; + } + + // Check IndexedDB + const indexedDBModel = await retrieveGLTF(asset.assetId!); + if (indexedDBModel) { + const blobUrl = URL.createObjectURL(indexedDBModel); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(asset.assetId!, gltf); + setGltfScene(gltf); + }, + undefined, + (error) => { + echo.error(`[IndexedDB] Error loading ${asset.modelName}:`); + URL.revokeObjectURL(blobUrl); + } + ); + return; + } + + // Fetch from Backend + const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${asset.assetId!}`; + loader.load(modelUrl, async (gltf) => { + const modelBlob = await fetch(modelUrl).then((res) => res.blob()); + await storeGLTF(asset.assetId!, modelBlob); + THREE.Cache.add(asset.assetId!, gltf); + setGltfScene(gltf); + }, + undefined, + (error) => { + echo.error(`[Backend] Error loading ${asset.modelName}:`); + } + ); + } catch (err) { + console.error("Failed to load model:", asset.assetId, err); + } + }; + + loadModel(); + + }, [asset.assetId]); + + return ( + <> + {gltfScene && + + + + } + + ); +} + +export default Model; \ No newline at end of file diff --git a/app/src/modules/builder/assetGroup/models/models.tsx b/app/src/modules/builder/assetGroup/models/models.tsx new file mode 100644 index 0000000..7378812 --- /dev/null +++ b/app/src/modules/builder/assetGroup/models/models.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { useAssetsStore } from '../../../../store/builder/useAssetStore'; +import Model from './model/model'; + +function Models() { + const { assets } = useAssetsStore(); + + return ( + <> + {assets.map((asset) => + + )} + + ) +} + +export default Models \ No newline at end of file diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index ed82590..57ead56 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -47,6 +47,7 @@ import MeasurementTool from "../scene/tools/measurementTool"; import NavMesh from "../simulation/vehicle/navMesh/navMesh"; import CalculateAreaGroup from "./groups/calculateAreaGroup"; import LayoutImage from "./layout/layoutImage"; +import AssetsGroup from "./assetGroup/assetsGroup"; export default function Builder() { const state = useThree(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. @@ -299,6 +300,8 @@ export default function Builder() { anglesnappedPoint={anglesnappedPoint} /> + {/* */} + diff --git a/app/src/modules/builder/geomentries/assets/assetManager.ts b/app/src/modules/builder/geomentries/assets/assetManager.ts index 889d905..5d2f90f 100644 --- a/app/src/modules/builder/geomentries/assets/assetManager.ts +++ b/app/src/modules/builder/geomentries/assets/assetManager.ts @@ -78,7 +78,7 @@ export default async function assetManager( }, undefined, (error) => { - toast.error(`[IndexedDB] Error loading ${item.modelName}:`); + echo.error(`[IndexedDB] Error loading ${item.modelName}:`); resolve(); } ); @@ -97,7 +97,7 @@ export default async function assetManager( }, undefined, (error) => { - toast.error(`[Backend] Error loading ${item.modelName}:`); + echo.error(`[Backend] Error loading ${item.modelName}:`); resolve(); } ); diff --git a/app/src/modules/builder/geomentries/lines/updateDistanceText.ts b/app/src/modules/builder/geomentries/lines/updateDistanceText.ts index 2f743a9..8a5a1cc 100644 --- a/app/src/modules/builder/geomentries/lines/updateDistanceText.ts +++ b/app/src/modules/builder/geomentries/lines/updateDistanceText.ts @@ -28,7 +28,7 @@ function updateDistanceText( const textMesh = text as THREE.Mesh; if (textMesh.userData[0][1] === linePoints[0][1] && textMesh.userData[1][1] === linePoints[1][1]) { textMesh.position.set(position.x, 1, position.z); - const className = `Distance line-${textMesh.userData[0][1]}_${textMesh.userData[1][1]}_${linePoints[0][2]}`; + const className = `distance line-${textMesh.userData[0][1]}_${textMesh.userData[1][1]}_${linePoints[0][2]}`; const element = document.getElementsByClassName(className)[0] as HTMLElement; if (element) { element.innerHTML = `${distance} m`; diff --git a/app/src/modules/builder/groups/floorItemsGroup.tsx b/app/src/modules/builder/groups/floorItemsGroup.tsx index fe2a03c..8abbfee 100644 --- a/app/src/modules/builder/groups/floorItemsGroup.tsx +++ b/app/src/modules/builder/groups/floorItemsGroup.tsx @@ -104,7 +104,7 @@ const FloorItemsGroup = ({ updateLoadingProgress(100); return; } - gltfLoaderWorker.postMessage({ floorItems: data }); + gltfLoaderWorker.postMessage({ floorItems: uniqueItems }); } else { gltfLoaderWorker.postMessage({ floorItems: [] }); loadInitialFloorItems( diff --git a/app/src/store/builder/useAssetStore.ts b/app/src/store/builder/useAssetStore.ts new file mode 100644 index 0000000..326a877 --- /dev/null +++ b/app/src/store/builder/useAssetStore.ts @@ -0,0 +1,221 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; + +interface AssetsStore { + assets: Assets; + + // Asset CRUD operations + addAsset: (asset: Asset) => void; + removeAsset: (modelUuid: string) => void; + updateAsset: (modelUuid: string, updates: Partial) => void; + setAssets: (assets: Assets) => void; + + // Asset properties + setPosition: (modelUuid: string, position: [number, number, number]) => void; + setRotation: (modelUuid: string, rotation: [number, number, number]) => void; + setLock: (modelUuid: string, isLocked: boolean) => void; + setCollision: (modelUuid: string, isCollidable: boolean) => void; + setVisibility: (modelUuid: string, isVisible: boolean) => void; + setOpacity: (modelUuid: string, opacity: number) => void; + + // Animation controls + setAnimation: (modelUuid: string, animation: string) => void; + setCurrentAnimation: (modelUuid: string, current: string, isPlaying: boolean) => void; + addAnimation: (modelUuid: string, animation: string) => void; + removeAnimation: (modelUuid: string, animation: string) => void; + + // Event data operations + addEventData: (modelUuid: string, eventData: Asset['eventData']) => void; + updateEventData: (modelUuid: string, updates: Partial) => void; + removeEventData: (modelUuid: string) => void; + + // Helper functions + getAssetById: (modelUuid: string) => Asset | undefined; + getAssetByPointUuid: (pointUuid: string) => Asset | undefined; + hasAsset: (modelUuid: string) => boolean; +} + +export const useAssetsStore = create()( + immer((set, get) => ({ + assets: [], + + // Asset CRUD operations + addAsset: (asset) => { + set((state) => { + if (!state.assets.some(a => a.modelUuid === asset.modelUuid)) { + state.assets.push(asset); + } + }); + }, + + removeAsset: (modelUuid) => { + set((state) => { + state.assets = state.assets.filter(a => a.modelUuid !== modelUuid); + }); + }, + + updateAsset: (modelUuid, updates) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + Object.assign(asset, updates); + } + }); + }, + + setAssets: (assets) => { + set((state) => { + state.assets = assets; + }); + }, + + // Asset properties + setPosition: (modelUuid, position) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.position = position; + } + }); + }, + + setRotation: (modelUuid, rotation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.rotation = rotation; + } + }); + }, + + setLock: (modelUuid, isLocked) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.isLocked = isLocked; + } + }); + }, + + setCollision: (modelUuid, isCollidable) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.isCollidable = isCollidable; + } + }); + }, + + setVisibility: (modelUuid, isVisible) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.isVisible = isVisible; + } + }); + }, + + setOpacity: (modelUuid, opacity) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.opacity = opacity; + } + }); + }, + + // Animation controls + setAnimation: (modelUuid, animation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + if (!asset.animationState) { + asset.animationState = { current: animation, playing: false }; + } else { + asset.animationState.current = animation; + } + } + }); + }, + + setCurrentAnimation: (modelUuid, current, isPlaying) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset?.animationState) { + asset.animationState.current = current; + asset.animationState.playing = isPlaying; + } + }); + }, + + addAnimation: (modelUuid, animation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + if (!asset.animations) { + asset.animations = [animation]; + } else if (!asset.animations.includes(animation)) { + asset.animations.push(animation); + } + } + }); + }, + + removeAnimation: (modelUuid, animation) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset?.animations) { + asset.animations = asset.animations.filter(a => a !== animation); + if (asset.animationState?.current === animation) { + asset.animationState.playing = false; + asset.animationState.current = ''; + } + } + }); + }, + + // Event data operations + addEventData: (modelUuid, eventData) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + asset.eventData = eventData; + } + }); + }, + + updateEventData: (modelUuid, updates) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset?.eventData) { + asset.eventData = { ...asset.eventData, ...updates }; + } + }); + }, + + removeEventData: (modelUuid) => { + set((state) => { + const asset = state.assets.find(a => a.modelUuid === modelUuid); + if (asset) { + delete asset.eventData; + } + }); + }, + + // Helper functions + getAssetById: (modelUuid) => { + return get().assets.find(a => a.modelUuid === modelUuid); + }, + + getAssetByPointUuid: (pointUuid) => { + return get().assets.find(asset => + asset.eventData?.point?.uuid === pointUuid || + asset.eventData?.points?.some(p => p.uuid === pointUuid) + ); + }, + + hasAsset: (modelUuid) => { + return get().assets.some(a => a.modelUuid === modelUuid); + } + })) +); \ No newline at end of file diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts new file mode 100644 index 0000000..adfdc78 --- /dev/null +++ b/app/src/types/builderTypes.d.ts @@ -0,0 +1,31 @@ +interface Asset { + modelUuid: string; + modelName: string; + assetId: string; + position: [number, number, number]; + rotation: [number, number, number]; + isLocked: boolean; + isCollidable: boolean; + isVisible: boolean; + opacity: number; + animations?: string[]; + animationState?: { + current: string; + playing: boolean; + }; + eventData?: { + type: string; + point?: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + } + points?: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + }[]; + } +}; + +type Assets = Asset[]; \ No newline at end of file