diff --git a/app/src/modules/builder/asset/assetsGroup.tsx b/app/src/modules/builder/asset/assetsGroup.tsx index 5227dd1..62c5d18 100644 --- a/app/src/modules/builder/asset/assetsGroup.tsx +++ b/app/src/modules/builder/asset/assetsGroup.tsx @@ -5,36 +5,29 @@ import { useThree } from "@react-three/fiber"; import { CameraControls } from "@react-three/drei"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; -import { useSocketStore } from "../../../store/socket/useSocketStore"; import useModuleStore from "../../../store/ui/useModuleStore"; import { useSceneContext } from "../../scene/sceneContext"; import { useLoadingProgress, useRenameModeStore, useSelectedItem } from "../../../store/builder/store"; import { useLeftData, useTopData } from "../../../store/visualization/useZone3DWidgetStore"; -import { getUserData } from "../../../functions/getUserData"; -import useAssetResponseHandler from "../../collaboration/responseHandler/useAssetResponseHandler"; import Models from "./models/models"; -import addAssetModel from "./functions/addAssetModel"; import { getFloorAssets } from "../../../services/factoryBuilder/asset/floorAsset/getFloorItemsApi"; +import useAddAssetModel from "./hooks/useAddAssetModel"; const gltfLoaderWorker = new Worker(new URL("../../../services/factoryBuilder/webWorkers/gltfLoaderWorker.js", import.meta.url)); function AssetsGroup({ plane }: { readonly plane: React.MutableRefObject }) { const { activeModule } = useModuleStore(); - const { builderSocket } = useSocketStore(); - const { controls, gl, pointer, camera, raycaster, scene } = useThree(); + const { controls, gl, pointer } = useThree(); const { setLoadingProgress } = useLoadingProgress(); - const { assetStore, versionStore, undoRedo3DStore } = useSceneContext(); + const { assetStore, versionStore } = useSceneContext(); const { selectedVersion } = versionStore(); const { setAssets, clearAssets } = assetStore(); - const { addAssetToScene } = useAssetResponseHandler(); - const { selectedItem, setSelectedItem } = useSelectedItem(); + const { selectedItem } = useSelectedItem(); const { projectId } = useParams(); const { isRenameMode, setIsRenameMode } = useRenameModeStore(); - const { userId } = getUserData(); const { setTop } = useTopData(); const { setLeft } = useLeftData(); - const { push3D } = undoRedo3DStore(); const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); @@ -42,6 +35,8 @@ function AssetsGroup({ plane }: { readonly plane: React.MutableRefObject { if (!projectId || !selectedVersion) return; @@ -145,7 +140,7 @@ function AssetsGroup({ plane }: { readonly plane: React.MutableRefObject; } diff --git a/app/src/modules/builder/asset/functions/addAssetModel.ts b/app/src/modules/builder/asset/functions/addAssetModel.ts deleted file mode 100644 index 6343804..0000000 --- a/app/src/modules/builder/asset/functions/addAssetModel.ts +++ /dev/null @@ -1,263 +0,0 @@ -import * as THREE from "three"; -import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; -import { retrieveGLTF, storeGLTF } from "../../../../utils/indexDB/idbUtils"; -import { Socket } from "socket.io-client"; -import * as CONSTANTS from "../../../../types/world/worldConstants"; -import PointsCalculator from "../../../simulation/functions/pointsCalculator"; -import createEventData from "../../../simulation/functions/createEventData"; - -import { getUserData } from "../../../../functions/getUserData"; -import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; - -async function addAssetModel( - scene: THREE.Scene, - raycaster: THREE.Raycaster, - camera: THREE.Camera, - pointer: THREE.Vector2, - builderSocket: Socket | null, - selectedItem: any, - setSelectedItem: any, - addAssetToScene: (asset: Asset, callback?: () => void) => void, - push3D: (entry: UndoRedo3DTypes) => void, - plane: React.MutableRefObject, - loader: GLTFLoader, - selectedVersion?: Version | null, - projectId?: string, - userId?: string -): Promise { - ////////// 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 { - raycaster.setFromCamera(pointer, camera); - const wallFloorsGroup = scene.getObjectByName("Walls-Floors-Group") as THREE.Group | null; - const floorsGroup = scene.getObjectByName("Floors-Group") as THREE.Group | null; - const floorChildren = floorsGroup?.children ?? []; - const wallFloorChildren = wallFloorsGroup?.children ?? []; - const floorIntersections = raycaster.intersectObjects([...floorChildren, ...wallFloorChildren], 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); - if (intersectedFloor.distance < intersectedPlane.distance) { - if (intersectedFloor.object.userData.floorUuid) { - intersectPoint = new THREE.Vector3(intersectedFloor.point.x, intersectedFloor.object.userData.floorDepth, intersectedFloor.point.z); - } else { - intersectPoint = new THREE.Vector3(intersectedFloor.point.x, 0, intersectedFloor.point.z); - } - } else { - intersectPoint = 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) { - handleModelLoad(cachedModel, intersectPoint!, selectedItem, addAssetToScene, push3D, builderSocket, selectedVersion?.versionId || "", projectId, userId); - return; - } else { - const cachedModelBlob = await retrieveGLTF(selectedItem.id); - if (cachedModelBlob) { - 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, addAssetToScene, push3D, builderSocket, selectedVersion?.versionId || "", projectId, userId); - }); - } else { - loader.load(`${url_Backend_dwinzo}/api/v2/AssetFile/${selectedItem.id}`, async (gltf) => { - const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v2/AssetFile/${selectedItem.id}`).then((res) => res.blob()); - await storeGLTF(selectedItem.id, modelBlob); - THREE.Cache.add(selectedItem.id, gltf); - await handleModelLoad(gltf, intersectPoint!, selectedItem, addAssetToScene, push3D, builderSocket, selectedVersion?.versionId || "", projectId, userId); - }); - } - } - } - } catch { - echo.error("Failed to add asset"); - } finally { - setSelectedItem({}); - } -} - -async function handleModelLoad( - gltf: any, - intersectPoint: THREE.Vector3, - selectedItem: any, - addAssetToScene: (asset: Asset, callback?: () => void) => void, - push3D: (entry: UndoRedo3DTypes) => void, - builderSocket: Socket | null, - versionId: string, - projectId?: string, - userId?: string -) { - const { organization } = getUserData(); - const model = gltf.scene.clone(); - model.userData = { - name: selectedItem.name, - modelId: selectedItem.id, - modelUuid: model.uuid, - }; - model.position.set(intersectPoint.x, intersectPoint.y, intersectPoint.z); - model.scale.set(...CONSTANTS.assetConfig.defaultScaleAfterGsap); - - const newFloorItem: Asset = { - modelUuid: model.uuid, - modelName: selectedItem.name, - assetId: selectedItem.id, - position: [intersectPoint.x, intersectPoint.y, intersectPoint.z], - rotation: [0, 0, 0], - scale: [1, 1, 1], - isLocked: false, - isVisible: true, - isCollidable: false, - opacity: 1, - }; - - const socketData: any = { - organization, - modelUuid: newFloorItem.modelUuid, - modelName: newFloorItem.modelName, - assetId: newFloorItem.assetId, - position: newFloorItem.position, - rotation: newFloorItem.rotation, - scale: newFloorItem.scale, - isLocked: false, - isVisible: true, - socketId: builderSocket?.id || "", - versionId: versionId, - projectId: projectId, - userId: userId, - }; - - const apiData: any = { - modelUuid: newFloorItem.modelUuid, - modelName: newFloorItem.modelName, - position: newFloorItem.position, - assetId: newFloorItem.assetId, - rotation: newFloorItem.rotation, - scale: newFloorItem.scale, - isLocked: false, - isVisible: true, - versionId: versionId, - projectId: projectId || "", - }; - - if (selectedItem.type) { - const data = PointsCalculator(selectedItem.type, gltf.scene.clone(), new THREE.Vector3(...model.rotation)); - - if (!data?.points) return; - - const eventData: any = { type: selectedItem.type, subType: selectedItem.subType || "" }; - - const assetEvent = createEventData(newFloorItem, selectedItem.type, selectedItem.subType, data.points); - if (!assetEvent) return; - - if ("point" in assetEvent) { - eventData.point = { - uuid: assetEvent.point.uuid, - position: assetEvent.point.position, - rotation: assetEvent.point.rotation, - }; - } else if ("points" in assetEvent) { - eventData.points = assetEvent.points.map((point) => ({ - uuid: point.uuid, - position: point.position, - rotation: point.rotation, - })); - } - - socketData.eventData = JSON.parse(JSON.stringify(eventData)); - apiData.eventData = JSON.parse(JSON.stringify(eventData)); - } - - if (!builderSocket?.connected) { - // API - - setAssetsApi(apiData) - .then((data) => { - if (!data.message) { - echo.error(`Error adding asset: ${newFloorItem.modelUuid}`); - return; - } - if (data.message === "Model created successfully" && data.data) { - const model: Asset = { - modelUuid: data.data.modelUuid, - modelName: data.data.modelName, - assetId: data.data.assetId, - position: data.data.position, - rotation: data.data.rotation, - scale: data.data.scale, - isLocked: data.data.isLocked, - isCollidable: true, - isVisible: data.data.isVisible, - opacity: 1, - ...(data.data.eventData ? { eventData: data.data.eventData } : {}), - }; - - addAssetToScene(model, () => { - push3D({ - type: "Scene", - actions: [ - { - module: "builder", - actionType: "Asset-Add", - asset: { - type: "Asset", - timeStap: new Date().toISOString(), - assetData: model, - }, - }, - ], - }); - - echo.info(`Added asset: ${model.modelUuid}`); - }); - } else { - echo.error(`Error adding asset: ${data?.data?.modelName}`); - } - }) - .catch(() => { - echo.error(`Error adding asset: ${newFloorItem.modelUuid}`); - }); - } else { - // SOCKET - - push3D({ - type: "Scene", - actions: [ - { - module: "builder", - actionType: "Asset-Add", - asset: { - type: "Asset", - timeStap: new Date().toISOString(), - assetData: model, - }, - }, - ], - }); - - console.log("socketData: ", socketData); - builderSocket.emit("v1:model-asset:add", socketData); - } -} - -export default addAssetModel; diff --git a/app/src/modules/builder/asset/functions/generateUniqueAssetName.ts b/app/src/modules/builder/asset/functions/generateUniqueAssetName.ts new file mode 100644 index 0000000..33444a7 --- /dev/null +++ b/app/src/modules/builder/asset/functions/generateUniqueAssetName.ts @@ -0,0 +1,60 @@ +const generateUniqueAssetName = (baseName: string, existingAssets: Asset[], usedNames: Set = new Set()): string => { + // Extract the base name without any existing numbers + const baseMatch = baseName.match(/^(.*?)(?:\.(\d+))?$/); + const trueBaseName = baseMatch![1]; + const existingNumber = baseMatch![2] ? parseInt(baseMatch![2], 10) : 0; + + // Find all assets that match the true base name pattern (both existing and newly used) + const allUsedNumbers: number[] = []; + const pattern = new RegExp(`^${trueBaseName}(?:\\.(\\d+))?$`); + + // Check existing assets + existingAssets.forEach((asset) => { + const match = asset.modelName.match(pattern); + if (match) { + if (!match[1]) { + allUsedNumbers.push(0); + } else { + allUsedNumbers.push(parseInt(match[1], 10)); + } + } + }); + + // Check names we've already assigned in this copy operation + usedNames.forEach((usedName) => { + const match = usedName.match(pattern); + if (match) { + if (!match[1]) { + allUsedNumbers.push(0); + } else { + allUsedNumbers.push(parseInt(match[1], 10)); + } + } + }); + + // Also include the existing number from the copied asset + if (!allUsedNumbers.includes(existingNumber)) { + allUsedNumbers.push(existingNumber); + } + + if (allUsedNumbers.length === 0) { + return trueBaseName; + } + + const maxNumber = Math.max(...allUsedNumbers); + let nextNumber = maxNumber + 1; + + // Find the next available number that hasn't been used + while (allUsedNumbers.includes(nextNumber)) { + nextNumber++; + } + + if (nextNumber === 1 && existingNumber === 0) { + // If we're going from no number to first number, use "001" + return `${trueBaseName}.${"001"}`; + } + + return `${trueBaseName}.${nextNumber.toString().padStart(3, "0")}`; +}; + +export default generateUniqueAssetName; diff --git a/app/src/modules/builder/asset/hooks/useAddAssetModel.ts b/app/src/modules/builder/asset/hooks/useAddAssetModel.ts new file mode 100644 index 0000000..cfb4a61 --- /dev/null +++ b/app/src/modules/builder/asset/hooks/useAddAssetModel.ts @@ -0,0 +1,280 @@ +import * as THREE from "three"; +import { useCallback } from "react"; +import { Socket } from "socket.io-client"; +import { useParams } from "react-router-dom"; +import { useThree } from "@react-three/fiber"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { retrieveGLTF, storeGLTF } from "../../../../utils/indexDB/idbUtils"; +import * as CONSTANTS from "../../../../types/world/worldConstants"; +import { useSocketStore } from "../../../../store/socket/useSocketStore"; +import { useSelectedItem } from "../../../../store/builder/store"; +import { useSceneContext } from "../../../scene/sceneContext"; +import useAssetResponseHandler from "../../../collaboration/responseHandler/useAssetResponseHandler"; +import PointsCalculator from "../../../simulation/functions/pointsCalculator"; +import createEventData from "../../../simulation/functions/createEventData"; +import generateUniqueAssetName from "../functions/generateUniqueAssetName"; +import { getUserData } from "../../../../functions/getUserData"; +import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; + +interface UseAddAssetModelProps { + plane: React.MutableRefObject; + loader: GLTFLoader; +} + +function useAddAssetModel({ plane, loader }: UseAddAssetModelProps) { + const { builderSocket } = useSocketStore(); + const { pointer, camera, raycaster, scene } = useThree(); + const { versionStore, undoRedo3DStore, assetStore } = useSceneContext(); + const { assets } = assetStore(); + const { selectedVersion } = versionStore(); + const { addAssetToScene } = useAssetResponseHandler(); + const { selectedItem, setSelectedItem } = useSelectedItem(); + const { projectId } = useParams(); + const { userId } = getUserData(); + const { push3D } = undoRedo3DStore(); + + const handleModelLoad = useCallback( + async ( + gltf: any, + intersectPoint: THREE.Vector3, + selectedItem: any, + addAssetToScene: (asset: Asset, callback?: () => void) => void, + push3D: (entry: UndoRedo3DTypes) => void, + builderSocket: Socket | null, + versionId: string, + projectId?: string, + userId?: string + ) => { + const { organization } = getUserData(); + const model = gltf.scene.clone(); + const uniqueModelName = generateUniqueAssetName(selectedItem.name, assets); + + model.userData = { + name: uniqueModelName, + modelId: selectedItem.id, + modelUuid: model.uuid, + }; + model.position.set(intersectPoint.x, intersectPoint.y, intersectPoint.z); + model.scale.set(...CONSTANTS.assetConfig.defaultScaleAfterGsap); + + const newFloorItem: Asset = { + modelUuid: model.uuid, + modelName: uniqueModelName, + assetId: selectedItem.id, + position: [intersectPoint.x, intersectPoint.y, intersectPoint.z], + rotation: [0, 0, 0], + scale: [1, 1, 1], + isLocked: false, + isVisible: true, + isCollidable: false, + opacity: 1, + }; + + const socketData: any = { + organization, + modelUuid: newFloorItem.modelUuid, + modelName: newFloorItem.modelName, + assetId: newFloorItem.assetId, + position: newFloorItem.position, + rotation: newFloorItem.rotation, + scale: newFloorItem.scale, + isLocked: false, + isVisible: true, + socketId: builderSocket?.id || "", + versionId: versionId, + projectId: projectId, + userId: userId, + }; + + const apiData: any = { + modelUuid: newFloorItem.modelUuid, + modelName: newFloorItem.modelName, + position: newFloorItem.position, + assetId: newFloorItem.assetId, + rotation: newFloorItem.rotation, + scale: newFloorItem.scale, + isLocked: false, + isVisible: true, + versionId: versionId, + projectId: projectId || "", + }; + + if (selectedItem.type) { + const data = PointsCalculator(selectedItem.type, gltf.scene.clone(), new THREE.Vector3(...model.rotation)); + + if (!data?.points) return; + + const eventData: any = { type: selectedItem.type, subType: selectedItem.subType || "" }; + + const assetEvent = createEventData(newFloorItem, selectedItem.type, selectedItem.subType, data.points); + if (!assetEvent) return; + + if ("point" in assetEvent) { + eventData.point = { + uuid: assetEvent.point.uuid, + position: assetEvent.point.position, + rotation: assetEvent.point.rotation, + }; + } else if ("points" in assetEvent) { + eventData.points = assetEvent.points.map((point) => ({ + uuid: point.uuid, + position: point.position, + rotation: point.rotation, + })); + } + + socketData.eventData = JSON.parse(JSON.stringify(eventData)); + apiData.eventData = JSON.parse(JSON.stringify(eventData)); + newFloorItem.eventData = JSON.parse(JSON.stringify(eventData)); + } + + if (!builderSocket?.connected) { + // API + + setAssetsApi(apiData) + .then((data) => { + if (!data.message) { + echo.error(`Error adding asset: ${newFloorItem.modelUuid}`); + return; + } + if (data.message === "Model created successfully" && data.data) { + const model: Asset = { + modelUuid: data.data.modelUuid, + modelName: data.data.modelName, + assetId: data.data.assetId, + position: data.data.position, + rotation: data.data.rotation, + scale: data.data.scale, + isLocked: data.data.isLocked, + isCollidable: true, + isVisible: data.data.isVisible, + opacity: 1, + ...(data.data.eventData ? { eventData: data.data.eventData } : {}), + }; + + addAssetToScene(model, () => { + push3D({ + type: "Scene", + actions: [ + { + module: "builder", + actionType: "Asset-Add", + asset: { + type: "Asset", + timeStap: new Date().toISOString(), + assetData: model, + }, + }, + ], + }); + + echo.info(`Added asset: ${model.modelUuid}`); + }); + } else { + echo.error(`Error adding asset: ${data?.data?.modelName}`); + } + }) + .catch(() => { + echo.error(`Error adding asset: ${newFloorItem.modelUuid}`); + }); + } else { + // SOCKET + + push3D({ + type: "Scene", + actions: [ + { + module: "builder", + actionType: "Asset-Add", + asset: { + type: "Asset", + timeStap: new Date().toISOString(), + assetData: newFloorItem, + }, + }, + ], + }); + + builderSocket.emit("v1:model-asset:add", socketData); + } + }, + [assets] + ); + + const addAssetModel = useCallback(async (): Promise => { + if (!selectedItem.id) return; + + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + + try { + raycaster.setFromCamera(pointer, camera); + const wallFloorsGroup = scene.getObjectByName("Walls-Floors-Group") as THREE.Group | null; + const floorsGroup = scene.getObjectByName("Floors-Group") as THREE.Group | null; + const floorChildren = floorsGroup?.children ?? []; + const wallFloorChildren = wallFloorsGroup?.children ?? []; + const floorIntersections = raycaster.intersectObjects([...floorChildren, ...wallFloorChildren], 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); + if (intersectedFloor.distance < intersectedPlane.distance) { + if (intersectedFloor.object.userData.floorUuid) { + intersectPoint = new THREE.Vector3(intersectedFloor.point.x, intersectedFloor.object.userData.floorDepth, intersectedFloor.point.z); + } else { + intersectPoint = new THREE.Vector3(intersectedFloor.point.x, 0, intersectedFloor.point.z); + } + } else { + intersectPoint = 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) { + handleModelLoad(cachedModel, intersectPoint, selectedItem, addAssetToScene, push3D, builderSocket, selectedVersion?.versionId || "", projectId, userId); + return; + } else { + const cachedModelBlob = await retrieveGLTF(selectedItem.id); + if (cachedModelBlob) { + 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, addAssetToScene, push3D, builderSocket, selectedVersion?.versionId || "", projectId, userId); + }); + } else { + loader.load(`${url_Backend_dwinzo}/api/v2/AssetFile/${selectedItem.id}`, async (gltf) => { + const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v2/AssetFile/${selectedItem.id}`).then((res) => res.blob()); + await storeGLTF(selectedItem.id, modelBlob); + THREE.Cache.add(selectedItem.id, gltf); + await handleModelLoad(gltf, intersectPoint!, selectedItem, addAssetToScene, push3D, builderSocket, selectedVersion?.versionId || "", projectId, userId); + }); + } + } + } + } catch { + echo.error("Failed to add asset"); + } finally { + setSelectedItem({}); + } + }, [scene, raycaster, camera, pointer, builderSocket, selectedItem, setSelectedItem, addAssetToScene, push3D, plane, loader, selectedVersion, projectId, userId, handleModelLoad]); + + return addAssetModel; +} + +export default useAddAssetModel; diff --git a/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx b/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx index 7a51c94..f0cb902 100644 --- a/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx +++ b/app/src/modules/scene/controls/assetControls/cutCopyPasteControls3D.tsx @@ -11,6 +11,7 @@ import { getUserData } from "../../../../functions/getUserData"; import useAssetResponseHandler from "../../../collaboration/responseHandler/useAssetResponseHandler"; import createEventData from "../../../simulation/functions/createEventData"; +import generateUniqueAssetName from "../../../builder/asset/functions/generateUniqueAssetName"; import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { deleteFloorAssetApi } from "../../../../services/factoryBuilder/asset/floorAsset/deleteFloorAssetApi"; import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi"; @@ -29,7 +30,6 @@ const CutCopyPasteControls3D = () => { removeAsset, getAssetById, selectedAssets, - setSelectedAssets, copiedObjects, setCopiedObjects, pastedObjects, @@ -281,7 +281,7 @@ const CutCopyPasteControls3D = () => { } }; - const copySelection = () => { + const copySelection = useCallback(() => { if (selectedAssets.length > 0) { const newClones = selectedAssets.map((asset: any) => { const clone = SkeletonUtils.clone(asset); @@ -291,16 +291,20 @@ const CutCopyPasteControls3D = () => { setCopiedObjects(newClones); echo.info("Objects copied!"); } - }; + }, [selectedAssets, setCopiedObjects]); - const pasteCopiedObjects = () => { + const pasteCopiedObjects = useCallback(() => { if (copiedObjects.length > 0 && pastedObjects.length === 0) { const { center, relatives } = calculateRelativePositions(copiedObjects); setRelativePositions(relatives); + const usedNames = new Set(); const newPastedObjects = copiedObjects.map((obj: any) => { const clone = SkeletonUtils.clone(obj); clone.userData.modelUuid = THREE.MathUtils.generateUUID(); + const uniqueModelName = generateUniqueAssetName(obj.userData.modelName, assets, usedNames); + clone.userData.modelName = uniqueModelName; + usedNames.add(uniqueModelName); return clone; }); @@ -333,7 +337,7 @@ const CutCopyPasteControls3D = () => { }); } } - }; + }, [copiedObjects, pastedObjects.length, assets]); const addPastedObjects = () => { if (pastedObjects.length === 0) return; @@ -587,7 +591,6 @@ const CutCopyPasteControls3D = () => { setPastedObjects([]); setDuplicatedObjects([]); setRotatedObjects([]); - setSelectedAssets([]); setIsPasting(false); setCenterOffset(null); }; diff --git a/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx b/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx index 808b939..867c2b2 100644 --- a/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx +++ b/app/src/modules/scene/controls/assetControls/duplicationControls3D.tsx @@ -12,6 +12,7 @@ import { handleAssetPositionSnap } from "../selectionControls/selection3D/functi import useAssetResponseHandler from "../../../collaboration/responseHandler/useAssetResponseHandler"; import createEventData from "../../../simulation/functions/createEventData"; +import generateUniqueAssetName from "../../../builder/asset/functions/generateUniqueAssetName"; import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; const DuplicationControls3D = () => { @@ -22,21 +23,8 @@ const DuplicationControls3D = () => { const { assetStore, undoRedo3DStore, versionStore } = useSceneContext(); const { push3D } = undoRedo3DStore(); const { projectId } = useParams(); - const { - assets, - addAsset, - removeAsset, - getAssetById, - selectedAssets, - setSelectedAssets, - duplicatedObjects, - setDuplicatedObjects, - setPastedObjects, - movedObjects, - setMovedObjects, - rotatedObjects, - setRotatedObjects, - } = assetStore(); + const { assets, addAsset, removeAsset, getAssetById, selectedAssets, duplicatedObjects, setDuplicatedObjects, setPastedObjects, movedObjects, setMovedObjects, rotatedObjects, setRotatedObjects } = + assetStore(); const { updateAssetInScene, removeAssetFromScene } = useAssetResponseHandler(); const { selectedVersion } = versionStore(); const { userId, organization } = getUserData(); @@ -237,10 +225,14 @@ const DuplicationControls3D = () => { const duplicateSelection = useCallback(() => { if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { const positions: Record = {}; + const usedNames = new Set(); const newDuplicatedObjects = selectedAssets.map((obj: any) => { const clone = SkeletonUtils.clone(obj); clone.userData.modelUuid = THREE.MathUtils.generateUUID(); + const uniqueModelName = generateUniqueAssetName(obj.userData.modelName, assets, usedNames); + clone.userData.modelName = uniqueModelName; + usedNames.add(uniqueModelName); positions[clone.userData.modelUuid] = new THREE.Vector3().copy(obj.position); return clone; }); @@ -275,7 +267,7 @@ const DuplicationControls3D = () => { addAsset(asset); }); } - }, [selectedAssets, duplicatedObjects]); + }, [selectedAssets, duplicatedObjects.length, assets]); const addDuplicatedAssets = () => { if (duplicatedObjects.length === 0) return; @@ -529,7 +521,6 @@ const DuplicationControls3D = () => { setPastedObjects([]); setDuplicatedObjects([]); setRotatedObjects([]); - setSelectedAssets([]); setIsDuplicating(false); setDragOffset(null); setAxisConstraint(null); diff --git a/app/src/modules/scene/controls/assetControls/moveControls3D.tsx b/app/src/modules/scene/controls/assetControls/moveControls3D.tsx index e464d3b..6547691 100644 --- a/app/src/modules/scene/controls/assetControls/moveControls3D.tsx +++ b/app/src/modules/scene/controls/assetControls/moveControls3D.tsx @@ -32,7 +32,6 @@ function MoveControls3D() { updateAsset, getAssetById, selectedAssets, - setSelectedAssets, movedObjects, setMovedObjects, pastedObjects, @@ -521,7 +520,6 @@ function MoveControls3D() { setDuplicatedObjects([]); setMovedObjects([]); setRotatedObjects([]); - setSelectedAssets([]); setKeyEvent(""); }; diff --git a/app/src/modules/scene/controls/assetControls/rotateControls3D.tsx b/app/src/modules/scene/controls/assetControls/rotateControls3D.tsx index 77e160b..ae38bb5 100644 --- a/app/src/modules/scene/controls/assetControls/rotateControls3D.tsx +++ b/app/src/modules/scene/controls/assetControls/rotateControls3D.tsx @@ -30,7 +30,6 @@ function RotateControls3D() { rotatedObjects, setRotatedObjects, selectedAssets, - setSelectedAssets, movedObjects, setMovedObjects, pastedObjects, @@ -472,7 +471,6 @@ function RotateControls3D() { setDuplicatedObjects([]); setMovedObjects([]); setRotatedObjects([]); - setSelectedAssets([]); setIsIndividualRotating(false); }; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts index ca16720..b29d00d 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts +++ b/app/src/modules/scene/controls/selectionControls/selection3D/functions/handleAssetRotationSnap.ts @@ -6,7 +6,7 @@ export function handleAssetRotationSnap({ keyEvent, snapBaseRef, prevRotationRef, - wasShiftHeldRef + wasShiftHeldRef, }: { object: THREE.Object3D; pointerDeltaX: number; @@ -15,9 +15,9 @@ export function handleAssetRotationSnap({ prevRotationRef: React.MutableRefObject; wasShiftHeldRef: React.MutableRefObject; }): number { - const SHIFT_SPEED = 0.5; // Fine rotation speed - const NORMAL_SPEED = 5; // Normal rotation speed - const CTRL_SNAP_DEG = 15; // 15 degrees + const SHIFT_SPEED = 0.5; // Fine rotation speed + const NORMAL_SPEED = 5; // Normal rotation speed + const CTRL_SNAP_DEG = 15; // 15 degrees const CTRL_SHIFT_SNAP_DEG = 5; // 5 degrees const isShiftHeld = keyEvent.includes("Shift"); @@ -38,7 +38,7 @@ export function handleAssetRotationSnap({ const snapRad = THREE.MathUtils.degToRad(snapDeg); // Get current Y rotation from object's quaternion - const euler = new THREE.Euler().setFromQuaternion(object.quaternion, 'YXZ'); + const euler = new THREE.Euler().setFromQuaternion(object.quaternion, "YXZ"); let currentAngle = euler.y; // Initialize snap base on first frame @@ -71,4 +71,4 @@ export function handleAssetRotationSnap({ prevRotationRef.current = null; return deltaAngle; } -} \ No newline at end of file +}