import { useCallback, useEffect, useMemo, useRef } from "react"; import * as THREE from "three"; import { useThree } from "@react-three/fiber"; import { SelectionHelper } from "../helper/selectionHelper"; import { SelectionBox } from "three/examples/jsm/interactive/SelectionBox"; import useModuleStore from "../../../../../store/ui/useModuleStore"; import { useParams } from "react-router-dom"; import { getUserData } from "../../../../../functions/getUserData"; import { useSceneContext } from "../../../sceneContext"; import { useContextActionStore, useSocketStore, useToggleView, useToolMode } from "../../../../../store/builder/store"; import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi"; import DuplicationControls3D from "./duplicationControls3D"; import CopyPasteControls3D from "./copyPasteControls3D"; import MoveControls3D from "./moveControls3D"; import RotateControls3D from "./rotateControls3D"; import TransformControls3D from "./transformControls3D"; import { useBuilderStore } from "../../../../../store/builder/useBuilderStore"; import { deleteFloorAssetApi } from "../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorAssetApi"; const SelectionControls3D: React.FC = () => { const { camera, controls, gl, scene, raycaster, pointer } = useThree(); const { toggleView } = useToggleView(); const { activeModule } = useModuleStore(); const { toolMode } = useToolMode(); const boundingBoxRef = useRef(); const { socket } = useSocketStore(); const { contextAction, setContextAction } = useContextActionStore(); const { assetStore, eventStore, productStore, undoRedo3DStore, versionStore } = useSceneContext(); const { selectedDecal, selectedWall, selectedAisle, selectedFloor, selectedFloorAsset, selectedWallAsset } = useBuilderStore(); const { push3D } = undoRedo3DStore(); const { removeAsset, getAssetById, selectedAssets, setSelectedAssets, movedObjects, rotatedObjects, copiedObjects, pastedObjects, duplicatedObjects, setPastedObjects, setDuplicatedObjects } = assetStore(); const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); const { selectedVersion } = versionStore(); const { projectId } = useParams(); const isDragging = useRef(false); const isLeftMouseDown = useRef(false); const isSelecting = useRef(false); const isRightClick = useRef(false); const rightClickMoved = useRef(false); const isCtrlSelecting = useRef(false); const isShiftSelecting = useRef(false); const { userId, organization } = getUserData(); 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 === "deleteAsset") { setContextAction(null); deleteSelection(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [contextAction]); useEffect(() => { if (!camera || !scene || toggleView) return; const canvasElement = gl.domElement; canvasElement.tabIndex = 0; const helper = new SelectionHelper(gl); const onPointerDown = (event: PointerEvent) => { if (event.button === 2) { isRightClick.current = true; rightClickMoved.current = false; } else if (event.button === 0) { isSelecting.current = false; isCtrlSelecting.current = event.ctrlKey; isShiftSelecting.current = event.shiftKey; isLeftMouseDown.current = true; isDragging.current = false; if (event.ctrlKey && duplicatedObjects.length === 0) { if (controls) (controls as any).enabled = false; selectionBox.startPoint.set(pointer.x, pointer.y, 0); } } }; const onPointerMove = (event: PointerEvent) => { if (isRightClick.current) { rightClickMoved.current = true; } if (isLeftMouseDown.current) { isDragging.current = true; } isSelecting.current = true; if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting.current) { selectionBox.endPoint.set(pointer.x, pointer.y, 0); } }; const onPointerUp = (event: PointerEvent) => { if (event.button === 2 && !event.ctrlKey && !event.shiftKey) { isRightClick.current = false; if (!rightClickMoved.current) { // clearSelection(); } return; } if (isSelecting.current && isCtrlSelecting.current) { isCtrlSelecting.current = false; isSelecting.current = false; if (event.ctrlKey && duplicatedObjects.length === 0) { selectAssets(); } } else if (!isSelecting.current && selectedAssets.length > 0 && ((!event.ctrlKey && !event.shiftKey && pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) { clearSelection(); helper.enabled = true; isCtrlSelecting.current = false; } else if (controls) { (controls as any).enabled = true; } if (!isDragging.current && isLeftMouseDown.current && isShiftSelecting.current && event.shiftKey) { isShiftSelecting.current = false; isLeftMouseDown.current = false; isDragging.current = false; raycaster.setFromCamera(pointer, camera); const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => !intersect.object.name.includes("Roof") && !intersect.object.name.includes("MeasurementReference") && !intersect.object.name.includes("agv-collider") && !intersect.object.name.includes("zonePlane") && !intersect.object.name.includes("SelectionGroup") && !intersect.object.name.includes("selectionAssetGroup") && !intersect.object.name.includes("SelectionGroupBoundingBoxLine") && !intersect.object.name.includes("SelectionGroupBoundingBox") && !intersect.object.name.includes("SelectionGroupBoundingLine") && intersect.object.type !== "GridHelper"); if (intersects.length > 0) { const intersect = intersects[0]; const intersectObject = intersect.object; let currentObject: THREE.Object3D | null = intersectObject; while (currentObject) { if (currentObject.userData.modelUuid) { break; } currentObject = currentObject.parent || null; } if (currentObject) { const updatedSelections = new Set(selectedAssets); if (updatedSelections.has(currentObject)) { updatedSelections.delete(currentObject); } else { updatedSelections.add(currentObject); } const selected = Array.from(updatedSelections); setSelectedAssets(selected); } } } else if (controls) { (controls as any).enabled = true; } }; const onKeyDown = (event: KeyboardEvent) => { if (movedObjects.length > 0 || rotatedObjects.length > 0) return; if (event.key.toLowerCase() === "escape") { event.preventDefault(); clearSelection(); } if (event.key.toLowerCase() === "delete") { event.preventDefault(); deleteSelection(); } }; const onContextMenu = (event: MouseEvent) => { event.preventDefault(); if (!rightClickMoved.current) { // clearSelection(); } rightClickMoved.current = false; }; if (!toggleView && activeModule === "builder" && (toolMode === "cursor" || toolMode === "Move-Asset" || toolMode === "Rotate-Asset") && !selectedDecal && !selectedWall && !selectedAisle && !selectedFloor && !selectedFloorAsset && !selectedWallAsset && duplicatedObjects.length === 0 && pastedObjects.length === 0) { helper.enabled = true; canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("contextmenu", onContextMenu); canvasElement.addEventListener("keydown", onKeyDown); } else { helper.enabled = false; helper.dispose(); } return () => { canvasElement.removeEventListener("pointerdown", onPointerDown); canvasElement.removeEventListener("pointermove", onPointerMove); canvasElement.removeEventListener("contextmenu", onContextMenu); canvasElement.removeEventListener("pointerup", onPointerUp); canvasElement.removeEventListener("keydown", onKeyDown); helper.enabled = false; helper.dispose(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, rotatedObjects, activeModule, toolMode, selectedDecal, selectedWall, selectedAisle, selectedFloor, selectedFloorAsset, selectedWallAsset, productStore]); useEffect(() => { if (activeModule !== "builder" || (toolMode !== "cursor" && toolMode !== "Move-Asset" && toolMode !== "Rotate-Asset") || toggleView) { clearSelection(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeModule, toolMode, toggleView]); const selectAssets = useCallback(() => { selectionBox.endPoint.set(pointer.x, pointer.y, 0); if (controls) (controls as any).enabled = true; let selectedObjects = selectionBox.select(); let Objects = new Set(); selectedObjects.forEach((object) => { let currentObject: THREE.Object3D | null = object; while (currentObject) { if (currentObject.userData.modelUuid && !currentObject.userData.wallAssetType) { Objects.add(currentObject); break; } currentObject = currentObject.parent || null; } }); if (Objects.size === 0) { // clearSelection(); return; } const updatedSelections = new Set(selectedAssets); Objects.forEach((obj) => { updatedSelections.has(obj) ? updatedSelections.delete(obj) : updatedSelections.add(obj); }); const selected = Array.from(updatedSelections); setSelectedAssets(selected); }, [selectionBox, pointer, controls, selectedAssets, setSelectedAssets]); const clearSelection = () => { setPastedObjects([]); setDuplicatedObjects([]); setSelectedAssets([]); }; const deleteSelection = () => { if (selectedAssets.length > 0 && duplicatedObjects.length === 0 && pastedObjects.length === 0) { const undoActions: UndoRedo3DAction[] = []; const assetsToDelete: AssetData[] = []; const selectedUUIDs = selectedAssets.map((mesh: THREE.Object3D) => mesh.uuid); selectedAssets.forEach((selectedMesh: THREE.Object3D) => { const asset = getAssetById(selectedMesh.userData.modelUuid); if (!asset) return; if (!socket?.connected) { // REST deleteFloorAssetApi({ modelUuid: selectedMesh.userData.modelUuid, modelName: selectedMesh.userData.modelName, versionId: selectedVersion?.versionId || "", projectId: projectId || "", }); } else { // SOCKET const data = { organization, modelUuid: selectedMesh.userData.modelUuid, modelName: selectedMesh.userData.modelName, socketId: socket.id, projectId, versionId: selectedVersion?.versionId || "", userId, }; socket.emit("v1:model-asset:delete", data); } eventStore.getState().removeEvent(selectedMesh.uuid); const updatedEvents = productStore.getState().deleteEvent(selectedMesh.uuid); updatedEvents.forEach((event) => { updateBackend(productStore.getState().selectedProduct.productName, productStore.getState().selectedProduct.productUuid, projectId || "", event); }); removeAsset(selectedMesh.uuid); echo.success("Model Removed!"); selectedMesh.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(); } } }); assetsToDelete.push({ type: "Asset", assetData: asset, timeStap: new Date().toISOString(), }); }); if (assetsToDelete.length === 1) { undoActions.push({ module: "builder", actionType: "Asset-Delete", asset: assetsToDelete[0], }); } else { undoActions.push({ module: "builder", actionType: "Assets-Delete", assets: assetsToDelete, }); } push3D({ type: "Scene", actions: undoActions, }); selectedUUIDs.forEach((uuid: string) => { removeAsset(uuid); }); echo.warn("Selected models removed!"); clearSelection(); } }; return ( <> ); }; export default SelectionControls3D;