import * as THREE from "three"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import { SkeletonUtils } from "three-stdlib"; import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store"; import * as Types from "../../../../../types/world/worldTypes"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { useParams } from "react-router-dom"; import { getUserData } from "../../../../../functions/getUserData"; import { useSceneContext } from "../../../sceneContext"; import { useVersionContext } from "../../../../builder/version/versionContext"; // import { setAssetsApi } from "../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi"; const DuplicationControls3D = ({ duplicatedObjects, setDuplicatedObjects, setpastedObjects, movedObjects, setMovedObjects, rotatedObjects, setRotatedObjects, }: any) => { const { camera, controls, gl, scene, pointer, raycaster } = useThree(); const { toggleView } = useToggleView(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); const { socket } = useSocketStore(); const { assetStore, eventStore } = useSceneContext(); const { addEvent } = eventStore(); const { projectId } = useParams(); const { assets, addAsset, updateAsset, removeAsset, getAssetById } = assetStore(); const { selectedVersionStore } = useVersionContext(); const { selectedVersion } = selectedVersionStore(); const { userId, organization } = getUserData(); const [dragOffset, setDragOffset] = useState(null); const [initialPositions, setInitialPositions] = useState>({}); const [isDuplicating, setIsDuplicating] = useState(false); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => { const pointPosition = new THREE.Vector3().copy(point.position); return new THREE.Vector3().subVectors(pointPosition, hitPoint); }, []); useEffect(() => { if (!camera || !scene || toggleView) return; const canvasElement = gl.domElement; canvasElement.tabIndex = 0; let isPointerMoving = false; const onPointerDown = (event: PointerEvent) => { isPointerMoving = false; if (event.button === 0) mouseButtonsDown.current.left = true; if (event.button === 2) mouseButtonsDown.current.right = true; }; const onPointerMove = () => { isPointerMoving = true; }; const onPointerUp = (event: PointerEvent) => { if (event.button === 0) mouseButtonsDown.current.left = false; if (event.button === 2) mouseButtonsDown.current.right = false; if (!isPointerMoving && duplicatedObjects.length > 0 && event.button === 0) { event.preventDefault(); addDuplicatedAssets(); } if (!isPointerMoving && duplicatedObjects.length > 0 && event.button === 2) { event.preventDefault(); clearSelection(); duplicatedObjects.forEach((obj: THREE.Object3D) => { removeAsset(obj.userData.modelUuid); }); } }; const onKeyDown = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); if (keyCombination === "Ctrl+D" && movedObjects.length === 0 && rotatedObjects.length === 0) { duplicateSelection(); } if (keyCombination === "ESCAPE" && duplicatedObjects.length > 0) { event.preventDefault(); clearSelection(); duplicatedObjects.forEach((obj: THREE.Object3D) => { removeAsset(obj.userData.modelUuid); }); } }; if (!toggleView) { canvasElement.addEventListener("pointerdown", onPointerDown); canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("keydown", onKeyDown); } return () => { canvasElement.removeEventListener("pointerdown", onPointerDown); canvasElement.removeEventListener("pointermove", onPointerMove); canvasElement.removeEventListener("pointerup", onPointerUp); canvasElement.removeEventListener("keydown", onKeyDown); }; }, [assets, camera, controls, scene, toggleView, selectedAssets, duplicatedObjects, movedObjects, socket, rotatedObjects]); useFrame(() => { if (!isDuplicating || duplicatedObjects.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) { const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid); if (model) { const newOffset = calculateDragOffset(model, intersectionPoint); setDragOffset(newOffset); } return; } if (dragOffset) { const adjustedHit = new THREE.Vector3().addVectors(intersectionPoint, dragOffset); duplicatedObjects.forEach((duplicatedObject: THREE.Object3D) => { if (duplicatedObject.userData.modelUuid) { const initialPosition = initialPositions[duplicatedObject.userData.modelUuid]; if (initialPosition) { const relativeOffset = new THREE.Vector3().subVectors( initialPosition, initialPositions[duplicatedObjects[0].userData.modelUuid] ); const model = scene.getObjectByProperty("uuid", duplicatedObject.userData.modelUuid); const newPosition = new THREE.Vector3().addVectors(adjustedHit, relativeOffset); const positionArray: [number, number, number] = [newPosition.x, newPosition.y, newPosition.z]; if (model) { model.position.set(...positionArray); } } } }); } } }); const duplicateSelection = useCallback(() => { if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { const positions: Record = {}; const newDuplicatedObjects = selectedAssets.map((obj: any) => { const clone = SkeletonUtils.clone(obj); clone.userData.modelUuid = THREE.MathUtils.generateUUID(); positions[clone.userData.modelUuid] = new THREE.Vector3().copy(obj.position); return clone; }); setDuplicatedObjects(newDuplicatedObjects); setInitialPositions(positions); raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit) { const offset = calculateDragOffset(selectedAssets[0], hit); setDragOffset(offset); } setIsDuplicating(true); newDuplicatedObjects.forEach((obj: THREE.Object3D) => { const asset: Asset = { modelUuid: obj.userData.modelUuid, modelName: obj.userData.modelName, assetId: obj.userData.assetId, position: [obj.position.x, 0, obj.position.z], rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], isLocked: false, isVisible: true, isCollidable: false, opacity: 0.5, }; addAsset(asset); }); } }, [selectedAssets, duplicatedObjects]); const addDuplicatedAssets = () => { if (duplicatedObjects.length === 0) return; duplicatedObjects.forEach(async (duplicatedAsset: THREE.Object3D) => { if (duplicatedAsset) { const assetUuid = duplicatedAsset.userData.modelUuid; const asset = getAssetById(assetUuid); const model = scene.getObjectByProperty("uuid", duplicatedAsset.userData.modelUuid); if (!asset || !model) return; const position = new THREE.Vector3().copy(model.position); const newFloorItem: Types.FloorItemType = { modelUuid: duplicatedAsset.userData.modelUuid, modelName: duplicatedAsset.userData.modelName, assetId: duplicatedAsset.userData.assetId, position: [position.x, position.y, position.z], rotation: { x: asset.rotation[0], y: asset.rotation[1], z: asset.rotation[2] }, isLocked: false, isVisible: true, }; let updatedEventData = null; if (duplicatedAsset.userData.eventData) { updatedEventData = JSON.parse(JSON.stringify(duplicatedAsset.userData.eventData)); updatedEventData.modelUuid = newFloorItem.modelUuid; const eventData: any = { type: duplicatedAsset.userData.eventData.type, }; if (duplicatedAsset.userData.eventData.type === "Conveyor") { const ConveyorEvent: ConveyorEventSchema = { modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, position: newFloorItem.position, rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z], state: "idle", type: 'transfer', speed: 1, points: updatedEventData.points.map((point: any, index: number) => ({ uuid: THREE.MathUtils.generateUUID(), position: [point.position[0], point.position[1], point.position[2]], rotation: [point.rotation[0], point.rotation[1], point.rotation[2]], action: { actionUuid: THREE.MathUtils.generateUUID(), actionName: `Action 1`, actionType: 'default', material: 'Default Material', delay: 0, spawnInterval: 5, spawnCount: 1, triggers: [] } })) }; addEvent(ConveyorEvent); eventData.points = ConveyorEvent.points.map(point => ({ uuid: point.uuid, position: point.position, rotation: point.rotation })); } else if (duplicatedAsset.userData.eventData.type === "Vehicle") { const vehicleEvent: VehicleEventSchema = { modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, position: newFloorItem.position, rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z], state: "idle", type: "vehicle", speed: 1, point: { uuid: THREE.MathUtils.generateUUID(), position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]], rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]], action: { actionUuid: THREE.MathUtils.generateUUID(), actionName: "Action 1", actionType: "travel", unLoadDuration: 5, loadCapacity: 1, steeringAngle: 0, pickUpPoint: null, unLoadPoint: null, paths: { initPickup: [], pickupDrop: [], dropPickup: [], }, triggers: [] } } }; addEvent(vehicleEvent); eventData.point = { uuid: vehicleEvent.point.uuid, position: vehicleEvent.point.position, rotation: vehicleEvent.point.rotation }; } else if (duplicatedAsset.userData.eventData.type === "ArmBot") { const roboticArmEvent: RoboticArmEventSchema = { modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, position: newFloorItem.position, rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z], state: "idle", type: "roboticArm", speed: 1, point: { uuid: THREE.MathUtils.generateUUID(), position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]], rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]], actions: [ { actionUuid: THREE.MathUtils.generateUUID(), actionName: "Action 1", actionType: "pickAndPlace", process: { startPoint: null, endPoint: null }, triggers: [] } ] } }; addEvent(roboticArmEvent); eventData.point = { uuid: roboticArmEvent.point.uuid, position: roboticArmEvent.point.position, rotation: roboticArmEvent.point.rotation }; } else if (duplicatedAsset.userData.eventData.type === "StaticMachine") { const machineEvent: MachineEventSchema = { modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, position: newFloorItem.position, rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z], state: "idle", type: "machine", point: { uuid: THREE.MathUtils.generateUUID(), position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]], rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]], action: { actionUuid: THREE.MathUtils.generateUUID(), actionName: "Action 1", actionType: "process", processTime: 10, swapMaterial: "Default Material", triggers: [] } } }; addEvent(machineEvent); eventData.point = { uuid: machineEvent.point.uuid, position: machineEvent.point.position, rotation: machineEvent.point.rotation }; } else if (duplicatedAsset.userData.eventData.type === "Storage") { const storageEvent: StorageEventSchema = { modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, position: newFloorItem.position, rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z], state: "idle", type: "storageUnit", point: { uuid: THREE.MathUtils.generateUUID(), position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]], rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]], action: { actionUuid: THREE.MathUtils.generateUUID(), actionName: "Action 1", actionType: "store", storageCapacity: 10, triggers: [] } } } addEvent(storageEvent); eventData.point = { uuid: storageEvent.point.uuid, position: storageEvent.point.position, rotation: storageEvent.point.rotation }; } else if (duplicatedAsset.userData.eventData.type === "Human") { const humanEvent: HumanEventSchema = { modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, position: newFloorItem.position, rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z], state: "idle", type: "human", speed: 1, point: { uuid: THREE.MathUtils.generateUUID(), position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]], rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]], actions: [ { actionUuid: THREE.MathUtils.generateUUID(), actionName: "Action 1", actionType: "worker", loadCapacity: 1, assemblyCount: 1, loadCount: 1, processTime: 10, triggers: [] } ] } } addEvent(humanEvent); eventData.point = { uuid: humanEvent.point.uuid, position: humanEvent.point.position, rotation: humanEvent.point.rotation }; } newFloorItem.eventData = eventData; const data = { organization, modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, assetId: newFloorItem.assetId, position: [position.x, position.y, position.z], rotation: { x: duplicatedAsset.rotation.x, y: duplicatedAsset.rotation.y, z: duplicatedAsset.rotation.z }, isLocked: false, isVisible: true, socketId: socket.id, eventData: eventData, versionId: selectedVersion?.versionId || '', userId, projectId }; //REST // await setAssetsApi(data); //SOCKET socket.emit("v1:model-asset:add", data); const asset: Asset = { modelUuid: data.modelUuid, modelName: data.modelName, assetId: data.assetId, position: [position.x, position.y, position.z], rotation: [data.rotation.x, data.rotation.y, data.rotation.z], isLocked: data.isLocked, isCollidable: false, isVisible: data.isVisible, opacity: 1, eventData: data.eventData }; updateAsset(asset.modelUuid, asset); } else { const data = { organization, modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, assetId: newFloorItem.assetId, position: [position.x, position.y, position.z], rotation: { x: duplicatedAsset.rotation.x, y: duplicatedAsset.rotation.y, z: duplicatedAsset.rotation.z }, isLocked: false, isVisible: true, socketId: socket.id, versionId: selectedVersion?.versionId || '', projectId, userId }; socket.emit("v1:model-asset:add", data); const asset: Asset = { modelUuid: data.modelUuid, modelName: data.modelName, assetId: data.assetId, position: [position.x, position.y, position.z], rotation: [data.rotation.x, data.rotation.y, data.rotation.z], isLocked: data.isLocked, isCollidable: false, isVisible: data.isVisible, opacity: 1, }; updateAsset(asset.modelUuid, asset); } } }); echo.success("Object duplicated!"); clearSelection(); }; const clearSelection = () => { setMovedObjects([]); setpastedObjects([]); setDuplicatedObjects([]); setRotatedObjects([]); setSelectedAssets([]); setIsDuplicating(false); setDragOffset(null); }; return null; }; export default DuplicationControls3D;