import * as THREE from "three"; import { useParams } from "react-router-dom"; import { useThree } from "@react-three/fiber"; import { TransformControls } from "@react-three/drei"; import { useRef, useEffect, useState, useCallback } from "react"; import { useToolMode } from "../../../../store/builder/store"; import { useSocketStore } from "../../../../store/socket/useSocketStore"; import { useSceneContext } from "../../sceneContext"; import useAssetResponseHandler from "../../../collaboration/responseHandler/useAssetResponseHandler"; import { getUserData } from "../../../../functions/getUserData"; import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys"; import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; import { updateEventToBackend } from "../../../../components/layout/sidebarRight/properties/eventProperties/functions/handleUpdateEventToBackend"; function TransformControls3D() { const { toolMode } = useToolMode(); const { camera, scene, gl } = useThree(); const transformControlsRef = useRef(null); const [visible, setVisible] = useState(false); const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">(""); const { builderSocket, simulationSocket } = useSocketStore(); const { assetStore, eventStore, productStore, undoRedo3DStore, versionStore } = useSceneContext(); const { selectedProduct, peekUpdateEvent, updateEvent, getEventByModelUuid } = productStore(); const { push3D } = undoRedo3DStore(); const { getAssetById, selectedAssets, setSelectedAssets, assets } = assetStore(); const { updateAssetInScene } = useAssetResponseHandler(); const { userId, organization } = getUserData(); const { selectedVersion } = versionStore(); const { projectId } = useParams(); const pivotPointRef = useRef(new THREE.Vector3()); const initialPositionsRef = useRef>(new Map()); const initialRotationsRef = useRef>(new Map()); const initialPivotPositionRef = useRef(new THREE.Vector3()); const initialQuaternionsRef = useRef>(new Map()); const tempObjectRef = useRef(new THREE.Object3D()); const updateBackend = (eventData: EventsSchema) => { updateEventToBackend({ productName: selectedProduct.productName, productUuid: selectedProduct.productUuid, projectId: projectId || "", eventData, simulationSocket, selectedVersion, updateEvent, }); }; const recalcGizmo = useCallback(() => { if (selectedAssets.length === 0) return; const bbox = new THREE.Box3(); selectedAssets.forEach((obj) => bbox.expandByObject(obj)); bbox.getCenter(pivotPointRef.current); initialPivotPositionRef.current.copy(pivotPointRef.current); tempObjectRef.current.position.copy(pivotPointRef.current); tempObjectRef.current.rotation.set(0, 0, 0); tempObjectRef.current.scale.set(1, 1, 1); initialPositionsRef.current.clear(); initialRotationsRef.current.clear(); initialQuaternionsRef.current.clear(); selectedAssets.forEach((obj) => { initialPositionsRef.current.set(obj.uuid, obj.position.clone()); initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); }); }, [selectedAssets, assets]); useEffect(() => { recalcGizmo(); }, [assets]); const handleTransformationComplete = useCallback(() => { if (selectedAssets.length === 0) return; const updatedAssets = [...selectedAssets]; setSelectedAssets(updatedAssets); selectedAssets.forEach((obj) => { const asset = getAssetById(obj.uuid); if (!asset) return; if (asset.eventData) { const productData = getEventByModelUuid(selectedProduct.productUuid, asset.modelUuid); if (productData) { const event = peekUpdateEvent(selectedProduct.productUuid, asset.modelUuid, { position: [obj.position.x, obj.position.y, obj.position.z], rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], }); if (event) { updateBackend(event); } } } if (!builderSocket?.connected) { // API setAssetsApi({ modelUuid: asset.modelUuid, modelName: asset.modelName, assetId: asset.assetId, position: [obj.position.x, obj.position.y, obj.position.z], rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], scale: asset.scale, isCollidable: asset.isCollidable, opacity: asset.opacity, isLocked: asset.isLocked, isVisible: asset.isVisible, versionId: selectedVersion?.versionId || "", projectId: projectId || "", }) .then((data) => { if (!data.message || !data.data) { echo.error(`Error moving asset: ${asset.modelUuid}`); 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, scale: data.data.scale, isLocked: data.data.isLocked, isVisible: data.data.isVisible, isCollidable: data.data.isCollidable, opacity: data.data.opacity, ...(data.data.eventData ? { eventData: data.data.eventData } : {}), }; updateAssetInScene(model, () => { echo.info(`Moved asset: ${model.modelName}`); }); } else { echo.error(`Error moving asset: ${asset.modelUuid}`); } }) .catch(() => { echo.error(`Error moving asset: ${asset.modelUuid}`); }); } else { // SOCKET const data = { organization, modelUuid: asset.modelUuid, modelName: asset.modelName, assetId: asset.assetId, position: [obj.position.x, obj.position.y, obj.position.z], rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], isCollidable: asset.isCollidable, opacity: asset.opacity, isLocked: asset.isLocked, isVisible: asset.isVisible, socketId: builderSocket.id, versionId: selectedVersion?.versionId || "", userId, projectId, }; builderSocket.emit("v1:model-asset:add", data); } }); }, [selectedAssets, setSelectedAssets, getAssetById, eventStore, productStore, updateBackend, projectId, organization, builderSocket, selectedVersion, userId, push3D]); useEffect(() => { const temp = tempObjectRef.current; scene.add(temp); return () => { scene.remove(temp); }; }, [scene]); useEffect(() => { if (selectedAssets.length === 0) { setVisible(false); return; } const bbox = new THREE.Box3(); selectedAssets.forEach((obj) => bbox.expandByObject(obj)); bbox.getCenter(pivotPointRef.current); initialPivotPositionRef.current.copy(pivotPointRef.current); tempObjectRef.current.position.copy(pivotPointRef.current); tempObjectRef.current.rotation.set(0, 0, 0); tempObjectRef.current.scale.set(1, 1, 1); initialPositionsRef.current.clear(); initialRotationsRef.current.clear(); initialQuaternionsRef.current.clear(); selectedAssets.forEach((obj) => { initialPositionsRef.current.set(obj.uuid, obj.position.clone()); initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); }); setVisible(true); }, [selectedAssets]); useEffect(() => { const controls = transformControlsRef.current; if (!controls || !visible) return; controls.setSize(0.6); controls.attach(tempObjectRef.current); controls.setMode(toolMode === "Move-Asset" ? "translate" : "rotate"); const onObjectChange = () => { const temp = tempObjectRef.current; if (!temp) return; selectedAssets.forEach((obj) => { const initialPos = initialPositionsRef.current.get(obj.uuid); const initialQuat = initialQuaternionsRef.current.get(obj.uuid); if (!initialPos || !initialQuat) return; if (controls.mode === "translate") { const delta = new THREE.Vector3().copy(temp.position).sub(initialPivotPositionRef.current); obj.position.copy(initialPos).add(delta); } else if (controls.mode === "rotate") { const deltaQuat = temp.quaternion.clone(); const relPos = initialPos.clone().sub(initialPivotPositionRef.current); relPos.applyQuaternion(deltaQuat); obj.position.copy(initialPivotPositionRef.current).add(relPos); obj.quaternion.copy(deltaQuat).multiply(initialQuat); } }); }; const onMouseUp = () => { const assetsToUpdate: AssetData[] = []; const undoActions: UndoRedo3DAction[] = []; selectedAssets.forEach((obj) => { const asset = getAssetById(obj.uuid); if (!asset) return; const initialPos = initialPositionsRef.current.get(obj.uuid); const initialRot = initialRotationsRef.current.get(obj.uuid); if (!initialPos || !initialRot) return; const assetData: Asset = { ...asset, position: [initialPos.x, initialPos.y, initialPos.z], rotation: [initialRot.x, initialRot.y, initialRot.z], }; const newData: Asset = { ...asset, position: [obj.position.x, obj.position.y, obj.position.z], rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], }; assetsToUpdate.push({ type: "Asset", assetData, newData, timeStap: new Date().toISOString(), }); }); 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, }); } selectedAssets.forEach((obj) => { initialPositionsRef.current.set(obj.uuid, obj.position.clone()); initialRotationsRef.current.set(obj.uuid, obj.rotation.clone()); initialQuaternionsRef.current.set(obj.uuid, obj.quaternion.clone()); }); if (selectedAssets.length > 0) { const bbox = new THREE.Box3(); selectedAssets.forEach((obj) => bbox.expandByObject(obj)); bbox.getCenter(pivotPointRef.current); initialPivotPositionRef.current.copy(pivotPointRef.current); tempObjectRef.current.position.copy(pivotPointRef.current); tempObjectRef.current.rotation.set(0, 0, 0); } recalcGizmo(); handleTransformationComplete(); }; controls.addEventListener("objectChange", onObjectChange); controls.addEventListener("mouseUp", onMouseUp); return () => { controls.removeEventListener("objectChange", onObjectChange); controls.removeEventListener("mouseUp", onMouseUp); controls.detach(); }; }, [selectedAssets, toolMode, visible, handleTransformationComplete, selectedProduct]); useEffect(() => { const canvasElement = gl.domElement; const handleKeyDown = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); if (keyCombination !== keyEvent) { if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { setKeyEvent(keyCombination); } else { setKeyEvent(""); } } }; const onKeyUp = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); if (keyCombination === "") { setKeyEvent(""); } else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { setKeyEvent(keyCombination); } }; if (visible) { canvasElement.addEventListener("keydown", handleKeyDown); canvasElement.addEventListener("keyup", onKeyUp); } return () => { canvasElement.removeEventListener("keydown", handleKeyDown); canvasElement.removeEventListener("keyup", onKeyUp); }; }, [gl, visible, keyEvent]); if (!visible || (toolMode !== "Move-Asset" && toolMode !== "Rotate-Asset")) { return null; } return ( ); } export default TransformControls3D;