import * as THREE from "three"; import * as Types from "../../../../../types/world/worldTypes"; import { useParams } from "react-router-dom"; import { useFrame, useThree } from "@react-three/fiber"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useSceneContext } from "../../../sceneContext"; import useModuleStore from "../../../../../store/ui/useModuleStore"; import { useSocketStore } from "../../../../../store/socket/useSocketStore"; import { useContextActionStore, useToggleView, useToolMode } from "../../../../../store/builder/store"; import { handleAssetPositionSnap } from "./functions/handleAssetPositionSnap"; import DistanceFindingControls from "./distanceFindingControls"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { getUserData } from "../../../../../functions/getUserData"; import useAssetResponseHandler from "../../../../collaboration/responseHandler/useAssetResponseHandler"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; import { setAssetsApi } from "../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; function MoveControls3D({ boundingBoxRef }: any) { const { camera, controls, gl, scene, pointer, raycaster } = useThree(); const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); const { toggleView } = useToggleView(); const { toolMode } = useToolMode(); const { activeModule } = useModuleStore(); const { builderSocket } = useSocketStore(); const { userId, organization } = getUserData(); const { projectId } = useParams(); const { assetStore, eventStore, productStore, undoRedo3DStore, versionStore } = useSceneContext(); const { push3D } = undoRedo3DStore(); const { updateAsset, getAssetById, selectedAssets, setSelectedAssets, movedObjects, setMovedObjects, pastedObjects, setPastedObjects, duplicatedObjects, setDuplicatedObjects, rotatedObjects, setRotatedObjects, initialStates, setInitialState, } = assetStore(); const { updateAssetInScene } = useAssetResponseHandler(); const { selectedVersion } = versionStore(); const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); const [axisConstraint, setAxisConstraint] = useState<"x" | "z" | null>(null); const [dragOffset, setDragOffset] = useState(null); const [isMoving, setIsMoving] = useState(false); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false }); const { contextAction, setContextAction } = useContextActionStore(); const fineMoveBaseRef = useRef(null); const lastPointerPositionRef = useRef(null); const wasShiftHeldRef = useRef(false); const updateBackend = (productName: string, productUuid: string, projectId: string, eventData: EventsSchema) => { upsertProductOrEventApi({ productName: productName, productUuid: productUuid, projectId: projectId, eventDatas: eventData, versionId: selectedVersion?.versionId || "", }); }; useEffect(() => { if (contextAction === "moveAsset") { setContextAction(null); moveAssets(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [contextAction]); useEffect(() => { if (!camera || !scene || toggleView) return; const canvasElement = gl.domElement; canvasElement.tabIndex = 0; let isPointerMoving = false; const onPointerMove = () => { isPointerMoving = true; }; const onKeyUp = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); if (keyCombination === "") { setKeyEvent(""); } else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { setKeyEvent(keyCombination); } if (movedObjects[0]) { const intersectionPoint = new THREE.Vector3(); raycaster.setFromCamera(pointer, camera); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit) { const newOffset = calculateDragOffset(movedObjects[0], intersectionPoint); setDragOffset(newOffset); } } }; const onPointerDown = (event: PointerEvent) => { isPointerMoving = false; if (event.button === 0) mouseButtonsDown.current.left = true; if (event.button === 2) mouseButtonsDown.current.right = true; }; const onPointerUp = (event: PointerEvent) => { if (event.button === 0) mouseButtonsDown.current.left = false; if (event.button === 2) mouseButtonsDown.current.right = false; if (!isPointerMoving && movedObjects.length > 0 && event.button === 0) { event.preventDefault(); placeMovedAssets(); } if (!isPointerMoving && movedObjects.length > 0 && event.button === 2) { event.preventDefault(); resetToInitialPositions(); clearSelection(); setMovedObjects([]); } }; const onKeyDown = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0) return; if (isMoving && movedObjects.length > 0) { if (event.key.toLowerCase() === "x") { setAxisConstraint((prev) => (prev === "x" ? null : "x")); event.preventDefault(); return; } if (event.key.toLowerCase() === "z") { setAxisConstraint((prev) => (prev === "z" ? null : "z")); event.preventDefault(); return; } } if (keyCombination !== keyEvent) { if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { setKeyEvent(keyCombination); } else { setKeyEvent(""); } } if (keyCombination === "G") { if (selectedAssets.length > 0) { moveAssets(); } } if (keyCombination === "ESCAPE") { event.preventDefault(); resetToInitialPositions(); clearSelection(); setMovedObjects([]); } }; if (!toggleView && toolMode === "cursor") { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("keydown", onKeyDown); canvasElement?.addEventListener("keyup", onKeyUp); } return () => { canvasElement.removeEventListener("pointerdown", onPointerDown); canvasElement.removeEventListener("pointermove", onPointerMove); canvasElement.removeEventListener("pointerup", onPointerUp); canvasElement.removeEventListener("keydown", onKeyDown); canvasElement?.removeEventListener("keyup", onKeyUp); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [camera, controls, scene, toggleView, toolMode, selectedAssets, builderSocket, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent, initialStates, productStore]); useEffect(() => { if (activeModule !== "builder" || toolMode !== "cursor" || toggleView) { resetToInitialPositions(); setMovedObjects([]); } }, [activeModule, toolMode, toggleView]); const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => { const pointPosition = new THREE.Vector3().copy(point.position); return new THREE.Vector3().subVectors(pointPosition, hitPoint); }, []); const resetToInitialPositions = useCallback(() => { setTimeout(() => { movedObjects.forEach((movedObject: THREE.Object3D) => { if (movedObject.userData.modelUuid && initialStates[movedObject.uuid]) { const initialState = initialStates[movedObject.uuid]; updateAsset(movedObject.userData.modelUuid, { position: initialState.position, rotation: initialState.rotation, }); movedObject.position.set(initialState.position[0], initialState.position[1], initialState.position[2]); if (initialState.rotation) { movedObject.rotation.set(initialState.rotation[0], initialState.rotation[1], initialState.rotation[2]); } } }); setAxisConstraint(null); }, 50); }, [movedObjects, initialStates, updateAsset]); const resetToInitialPosition = useCallback( (modelUuid: string, callBack?: () => void) => { setTimeout(() => { const movedObject = movedObjects.find((obj: THREE.Object3D) => obj.userData.modelUuid === modelUuid); if (!movedObject) return; const initialState = initialStates[movedObject.uuid]; if (!initialState) return; updateAsset(modelUuid, { position: initialState.position, rotation: initialState.rotation, }); movedObject.position.set(initialState.position[0], initialState.position[1], initialState.position[2]); if (initialState.rotation) { movedObject.rotation.set(initialState.rotation[0], initialState.rotation[1], initialState.rotation[2]); } setAxisConstraint(null); if (callBack) callBack(); }, 50); }, [movedObjects, initialStates, updateAsset] ); useEffect(() => { if (movedObjects.length > 0) { const intersectionPoint = new THREE.Vector3(); raycaster.setFromCamera(pointer, camera); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit) { const newOffset = calculateDragOffset(movedObjects[0], intersectionPoint); setDragOffset(newOffset); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [axisConstraint, camera, movedObjects]); useFrame(() => { if (!isMoving || movedObjects.length === 0) return; const intersectionPoint = new THREE.Vector3(); raycaster.setFromCamera(pointer, camera); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit) { if (mouseButtonsDown.current.left || mouseButtonsDown.current.right) { if (movedObjects[0]) { const newOffset = calculateDragOffset(movedObjects[0], intersectionPoint); setDragOffset(newOffset); } return; } if (dragOffset) { const rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); const model = movedObjects[0]; const baseNewPosition = handleAssetPositionSnap({ rawBasePosition, intersectionPoint, model, axisConstraint, keyEvent, fineMoveBaseRef, lastPointerPositionRef, wasShiftHeldRef }); movedObjects.forEach((movedAsset: THREE.Object3D) => { if (movedAsset.userData.modelUuid) { const initialState = initialStates[movedAsset.userData.modelUuid]; const offsetState = initialStates[movedObjects[0].uuid]; if (initialState.position && offsetState.position) { const relativeOffset = new THREE.Vector3().subVectors( new THREE.Vector3(initialState.position[0], initialState.position[1], initialState.position[2]), new THREE.Vector3(offsetState.position[0], offsetState.position[1], offsetState.position[2]) ); const model = scene.getObjectByProperty("uuid", movedAsset.userData.modelUuid); const newPosition = new THREE.Vector3().addVectors(baseNewPosition, relativeOffset); const positionArray: [number, number, number] = [newPosition.x, newPosition.y, newPosition.z]; if (model) { model.position.set(...positionArray); } } } }); const position = new THREE.Vector3(); if (boundingBoxRef.current) { boundingBoxRef.current.getWorldPosition(position); } else { const box = new THREE.Box3(); movedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj)); const center = new THREE.Vector3(); box.getCenter(center); } } } }); const moveAssets = () => { const states: Record = {}; const positions: Record = {}; selectedAssets.forEach((asset: THREE.Object3D) => { states[asset.uuid] = { position: asset.position.toArray(), rotation: [asset.rotation.x, asset.rotation.y, asset.rotation.z], }; positions[asset.uuid] = new THREE.Vector3().copy(asset.position); }); setInitialState(states); raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit && selectedAssets[0]) { const offset = calculateDragOffset(selectedAssets[0], hit); setDragOffset(offset); } setMovedObjects(selectedAssets); setIsMoving(true); }; const placeMovedAssets = () => { if (movedObjects.length === 0) return; const undoActions: UndoRedo3DAction[] = []; const assetsToUpdate: AssetData[] = []; movedObjects.forEach(async (movedAsset: THREE.Object3D) => { if (movedAsset) { const assetUuid = movedAsset.userData.modelUuid; const asset = getAssetById(assetUuid); const model = scene.getObjectByProperty("uuid", movedAsset.userData.modelUuid); if (!asset || !model) return; const position = new THREE.Vector3().copy(model.position); const initialState = initialStates[assetUuid]; if (initialState) { assetsToUpdate.push({ type: "Asset", assetData: { ...asset, position: initialState.position, rotation: initialState.rotation, }, newData: { ...asset, position: [position.x, position.y, position.z], rotation: [movedAsset.rotation.x, movedAsset.rotation.y, movedAsset.rotation.z], }, timeStap: new Date().toISOString(), }); } const newFloorItem: Types.FloorItemType = { modelUuid: movedAsset.userData.modelUuid, modelName: movedAsset.userData.modelName, assetId: movedAsset.userData.assetId, position: [position.x, position.y, position.z], rotation: { x: movedAsset.rotation.x, y: movedAsset.rotation.y, z: movedAsset.rotation.z }, isLocked: false, isVisible: true, }; if (movedAsset.userData.eventData) { const eventData = eventStore.getState().getEventByModelUuid(movedAsset.userData.modelUuid); const productData = productStore.getState().getEventByModelUuid(productStore.getState().selectedProduct.productUuid, movedAsset.userData.modelUuid); if (eventData) { eventStore.getState().updateEvent(movedAsset.userData.modelUuid, { position: [position.x, position.y, position.z], rotation: [movedAsset.rotation.x, movedAsset.rotation.y, movedAsset.rotation.z], }); } if (productData) { const event = productStore.getState().updateEvent(productStore.getState().selectedProduct.productUuid, movedAsset.userData.modelUuid, { position: [position.x, position.y, position.z], rotation: [movedAsset.rotation.x, movedAsset.rotation.y, movedAsset.rotation.z], }); if (event) { updateBackend(productStore.getState().selectedProduct.productName, productStore.getState().selectedProduct.productUuid, projectId || "", event); } newFloorItem.eventData = eventData; } } const data = { organization, modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, assetId: newFloorItem.assetId, position: newFloorItem.position, rotation: { x: movedAsset.rotation.x, y: movedAsset.rotation.y, z: movedAsset.rotation.z }, isLocked: false, isVisible: true, socketId: builderSocket?.id, versionId: selectedVersion?.versionId || "", projectId, userId, }; if (!builderSocket?.connected) { // REST setAssetsApi({ modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, assetId: newFloorItem.assetId, position: newFloorItem.position, rotation: { x: movedAsset.rotation.x, y: movedAsset.rotation.y, z: movedAsset.rotation.z }, isLocked: false, isVisible: true, versionId: selectedVersion?.versionId || "", projectId: projectId, }) .then((data) => { if (!data.message || !data.data) { echo.error(`Error moving asset: ${newFloorItem.modelUuid}`); resetToInitialPosition(newFloorItem.modelUuid); clearSelection(); return; } if (data.message === "Model updated 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.x, data.data.rotation.y, data.data.rotation.z], isLocked: data.data.isLocked, isCollidable: true, isVisible: data.data.isVisible, opacity: 1, eventData: data.data.eventData, }; updateAssetInScene(model, () => { echo.log(`Moved asset: ${model.modelName}`); clearSelection(); }); } else { echo.error(`Error moving asset: ${newFloorItem.modelUuid}`); resetToInitialPosition(newFloorItem.modelUuid); clearSelection(); } }) .catch(() => { echo.error(`Error moving asset: ${newFloorItem.modelUuid}`); resetToInitialPosition(newFloorItem.modelUuid); clearSelection(); }); } else { // SOCKET builderSocket.emit("v1:model-asset:add", data); } } }); if (assetsToUpdate.length > 0) { if (assetsToUpdate.length === 1) { undoActions.push({ module: "builder", actionType: "Asset-Update", asset: assetsToUpdate[0], }); } else { undoActions.push({ module: "builder", actionType: "Assets-Update", assets: assetsToUpdate, }); } push3D({ type: "Scene", actions: undoActions, }); } setIsMoving(false); clearSelection(); setAxisConstraint(null); }; const clearSelection = () => { setPastedObjects([]); setDuplicatedObjects([]); setMovedObjects([]); setRotatedObjects([]); setSelectedAssets([]); setKeyEvent(""); }; return ; } export default MoveControls3D;