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 CopyPasteControls3D = ({ copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, setDuplicatedObjects, 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 [isPasting, setIsPasting] = useState(false); const [relativePositions, setRelativePositions] = useState([]); const [centerOffset, setCenterOffset] = useState(null); const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, }); const calculateRelativePositions = useCallback((objects: THREE.Object3D[]) => { if (objects.length === 0) return { center: new THREE.Vector3(), relatives: [] }; const box = new THREE.Box3(); objects.forEach(obj => box.expandByObject(obj)); const center = new THREE.Vector3(); box.getCenter(center); const relatives = objects.map(obj => { const relativePos = new THREE.Vector3().subVectors(obj.position, center); return relativePos; }); return { center, relatives }; }, []); 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 && pastedObjects.length > 0 && event.button === 0) { event.preventDefault(); addPastedObjects(); } if (!isPointerMoving && pastedObjects.length > 0 && event.button === 2) { event.preventDefault(); clearSelection(); pastedObjects.forEach((obj: THREE.Object3D) => { removeAsset(obj.userData.modelUuid); }); } }; const onKeyDown = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); if (keyCombination === "Ctrl+C" && movedObjects.length === 0 && rotatedObjects.length === 0) { copySelection(); } if (keyCombination === "Ctrl+V" && copiedObjects.length > 0 && pastedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { pasteCopiedObjects(); } if (keyCombination === "ESCAPE" && pastedObjects.length > 0) { event.preventDefault(); clearSelection(); pastedObjects.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, copiedObjects, pastedObjects, movedObjects, socket, rotatedObjects]); useFrame(() => { if (!isPasting || pastedObjects.length === 0) return; if (mouseButtonsDown.current.left || mouseButtonsDown.current.right) return; const intersectionPoint = new THREE.Vector3(); raycaster.setFromCamera(pointer, camera); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit && centerOffset) { pastedObjects.forEach((pastedObject: THREE.Object3D, index: number) => { const model = scene.getObjectByProperty("uuid", pastedObject.userData.modelUuid); if (!model) return; const newPos = new THREE.Vector3().addVectors(hit, relativePositions[index]); model.position.set(newPos.x, 0, newPos.z); }); } }); const copySelection = () => { if (selectedAssets.length > 0) { const newClones = selectedAssets.map((asset: any) => { const clone = SkeletonUtils.clone(asset); clone.position.copy(asset.position); return clone; }); setCopiedObjects(newClones); echo.info("Objects copied!"); } }; const pasteCopiedObjects = () => { if (copiedObjects.length > 0 && pastedObjects.length === 0) { const { center, relatives } = calculateRelativePositions(copiedObjects); setRelativePositions(relatives); const newPastedObjects = copiedObjects.map((obj: any) => { const clone = SkeletonUtils.clone(obj); clone.userData.modelUuid = THREE.MathUtils.generateUUID(); return clone; }); setpastedObjects(newPastedObjects); raycaster.setFromCamera(pointer, camera); const intersectionPoint = new THREE.Vector3(); const hit = raycaster.ray.intersectPlane(plane, intersectionPoint); if (hit) { const offset = new THREE.Vector3().subVectors(center, hit); setCenterOffset(offset); setIsPasting(true); newPastedObjects.forEach((obj: THREE.Object3D, index: number) => { const initialPos = new THREE.Vector3().addVectors(hit, relatives[index]); const asset: Asset = { modelUuid: obj.userData.modelUuid, modelName: obj.userData.modelName, assetId: obj.userData.assetId, position: initialPos.toArray(), rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], isLocked: false, isVisible: true, isCollidable: false, opacity: 0.5, }; addAsset(asset); }); } } }; const addPastedObjects = () => { if (pastedObjects.length === 0) return; pastedObjects.forEach(async (pastedAsset: THREE.Object3D) => { if (pastedAsset) { const assetUuid = pastedAsset.userData.modelUuid; const asset = getAssetById(assetUuid); const model = scene.getObjectByProperty("uuid", pastedAsset.userData.modelUuid); if (!asset || !model) return; const position = new THREE.Vector3().copy(model.position); const newFloorItem: Types.FloorItemType = { modelUuid: pastedAsset.userData.modelUuid, modelName: pastedAsset.userData.modelName, assetId: pastedAsset.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 (pastedAsset.userData.eventData) { updatedEventData = JSON.parse(JSON.stringify(pastedAsset.userData.eventData)); updatedEventData.modelUuid = newFloorItem.modelUuid; const eventData: any = { type: pastedAsset.userData.eventData.type, subType: pastedAsset.userData.eventData.subType, }; if (pastedAsset.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', subType: pastedAsset.userData.eventData.subType || '', 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 (pastedAsset.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", subType: pastedAsset.userData.eventData.subType || '', 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 (pastedAsset.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", subType: pastedAsset.userData.eventData.subType || '', 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 (pastedAsset.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", subType: pastedAsset.userData.eventData.subType || '', 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 (pastedAsset.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", subType: pastedAsset.userData.eventData.subType || '', 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 (pastedAsset.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", subType: pastedAsset.userData.eventData.subType || '', 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 }; } else if (pastedAsset.userData.eventData.type === "Crane") { const craneEvent: CraneEventSchema = { modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, position: newFloorItem.position, rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z], state: "idle", type: "crane", subType: pastedAsset.userData.eventData.subType || '', 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: "pickAndDrop", maxPickUpCount: 1, triggers: [] } ] } } addEvent(craneEvent); eventData.point = { uuid: craneEvent.point.uuid, position: craneEvent.point.position, rotation: craneEvent.point.rotation }; } newFloorItem.eventData = eventData; const data = { organization, modelUuid: newFloorItem.modelUuid, modelName: newFloorItem.modelName, assetId: newFloorItem.assetId, position: [position.x, 0, position.z], rotation: { x: pastedAsset.rotation.x, y: pastedAsset.rotation.y, z: pastedAsset.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, 0, 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, 0, position.z], rotation: { x: pastedAsset.rotation.x, y: pastedAsset.rotation.y, z: pastedAsset.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, 0, 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 added!"); clearSelection(); }; const clearSelection = () => { setMovedObjects([]); setpastedObjects([]); setDuplicatedObjects([]); setRotatedObjects([]); setSelectedAssets([]); setIsPasting(false); setCenterOffset(null); }; return null; }; export default CopyPasteControls3D;