614 lines
27 KiB
TypeScript
614 lines
27 KiB
TypeScript
import * as THREE from "three";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { useParams } from "react-router-dom";
|
|
import { SkeletonUtils } from "three-stdlib";
|
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
import { useSceneContext } from "../../sceneContext";
|
|
import { useSocketStore } from "../../../../store/socket/useSocketStore";
|
|
import { useContextActionStore, useToggleView } from "../../../../store/builder/store";
|
|
import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys";
|
|
import { getUserData } from "../../../../functions/getUserData";
|
|
import useAssetResponseHandler from "../../../collaboration/responseHandler/useAssetResponseHandler";
|
|
|
|
import createEventData from "../../../simulation/functions/createEventData";
|
|
import generateUniqueAssetName from "../../../builder/asset/functions/generateUniqueAssetName";
|
|
import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
|
|
import { deleteFloorAssetApi } from "../../../../services/factoryBuilder/asset/floorAsset/deleteFloorAssetApi";
|
|
import { updateEventToBackend } from "../../../../components/layout/sidebarRight/properties/eventProperties/functions/handleUpdateEventToBackend";
|
|
|
|
const CutCopyPasteControls3D = () => {
|
|
const { camera, controls, gl, scene, pointer, raycaster } = useThree();
|
|
const { toggleView } = useToggleView();
|
|
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
|
|
const { builderSocket, simulationSocket } = useSocketStore();
|
|
const { assetStore, undoRedo3DStore, versionStore, productStore } = useSceneContext();
|
|
const { push3D } = undoRedo3DStore();
|
|
const { updateEvent, deleteEvent, selectedProduct } = productStore();
|
|
const { projectId } = useParams();
|
|
const {
|
|
assets,
|
|
addAsset,
|
|
removeAsset,
|
|
getAssetById,
|
|
selectedAssets,
|
|
copiedObjects,
|
|
setCopiedObjects,
|
|
pastedObjects,
|
|
setPastedObjects,
|
|
duplicatedObjects,
|
|
setDuplicatedObjects,
|
|
movedObjects,
|
|
setMovedObjects,
|
|
rotatedObjects,
|
|
setRotatedObjects,
|
|
} = assetStore();
|
|
const { updateAssetInScene, removeAssetFromScene } = useAssetResponseHandler();
|
|
const { selectedVersion } = versionStore();
|
|
const { userId, organization } = getUserData();
|
|
|
|
const [isPasting, setIsPasting] = useState(false);
|
|
const [relativePositions, setRelativePositions] = useState<THREE.Vector3[]>([]);
|
|
const [centerOffset, setCenterOffset] = useState<THREE.Vector3 | null>(null);
|
|
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false });
|
|
const { contextAction, setContextAction } = useContextActionStore();
|
|
|
|
const updateBackend = (eventData: EventsSchema) => {
|
|
updateEventToBackend({
|
|
productName: selectedProduct.productName,
|
|
productUuid: selectedProduct.productUuid,
|
|
projectId: projectId || "",
|
|
eventData,
|
|
simulationSocket,
|
|
selectedVersion,
|
|
updateEvent,
|
|
});
|
|
};
|
|
|
|
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 (contextAction === "copyAsset") {
|
|
setContextAction(null);
|
|
copySelection();
|
|
} else if (contextAction === "pasteAsset") {
|
|
setContextAction(null);
|
|
pasteCopiedObjects();
|
|
}
|
|
}, [contextAction]);
|
|
|
|
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+X" && movedObjects.length === 0 && rotatedObjects.length === 0) {
|
|
cutSelection();
|
|
}
|
|
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, duplicatedObjects, movedObjects, builderSocket, rotatedObjects, selectedProduct]);
|
|
|
|
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 cutSelection = () => {
|
|
if (selectedAssets.length > 0) {
|
|
const newClones = selectedAssets.map((asset: any) => {
|
|
const clone = SkeletonUtils.clone(asset);
|
|
clone.position.copy(asset.position);
|
|
return clone;
|
|
});
|
|
setCopiedObjects(newClones);
|
|
|
|
if (duplicatedObjects.length === 0 && pastedObjects.length === 0) {
|
|
const undoActions: UndoRedo3DAction[] = [];
|
|
const assetsToDelete: AssetData[] = [];
|
|
|
|
selectedAssets.forEach((selectedMesh: THREE.Object3D) => {
|
|
const asset = getAssetById(selectedMesh.userData.modelUuid);
|
|
if (!asset) return;
|
|
|
|
if (!builderSocket?.connected) {
|
|
// REST
|
|
|
|
deleteFloorAssetApi({
|
|
modelUuid: selectedMesh.userData.modelUuid,
|
|
modelName: selectedMesh.userData.modelName,
|
|
versionId: selectedVersion?.versionId || "",
|
|
projectId: projectId || "",
|
|
}).then((data) => {
|
|
if (!data.message || !data.data) {
|
|
echo.error(`Error removing asset`);
|
|
return;
|
|
}
|
|
if (data.message === "Model deleted successfully") {
|
|
removeAssetFromScene(data.data.modelUuid, () => {
|
|
echo.info(`Removed asset: ${data.data.modelName}`);
|
|
});
|
|
} else {
|
|
echo.error(`Error removing asset: ${data?.data?.modelName}`);
|
|
}
|
|
});
|
|
} else {
|
|
// SOCKET
|
|
|
|
const data = {
|
|
organization,
|
|
modelUuid: selectedMesh.userData.modelUuid,
|
|
modelName: selectedMesh.userData.modelName,
|
|
socketId: builderSocket.id,
|
|
projectId,
|
|
versionId: selectedVersion?.versionId || "",
|
|
userId,
|
|
};
|
|
|
|
builderSocket.emit("v1:model-asset:delete", data);
|
|
}
|
|
|
|
const updatedEvents = deleteEvent(selectedMesh.uuid);
|
|
|
|
updatedEvents.forEach((event) => {
|
|
updateBackend(event);
|
|
});
|
|
|
|
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,
|
|
});
|
|
|
|
clearSelection();
|
|
}
|
|
}
|
|
};
|
|
|
|
const copySelection = useCallback(() => {
|
|
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!");
|
|
}
|
|
}, [selectedAssets, setCopiedObjects]);
|
|
|
|
const pasteCopiedObjects = useCallback(() => {
|
|
if (copiedObjects.length > 0 && pastedObjects.length === 0) {
|
|
const { center, relatives } = calculateRelativePositions(copiedObjects);
|
|
setRelativePositions(relatives);
|
|
const usedNames = new Set<string>();
|
|
|
|
const newPastedObjects = copiedObjects.map((obj: any) => {
|
|
const clone = SkeletonUtils.clone(obj);
|
|
clone.userData.modelUuid = THREE.MathUtils.generateUUID();
|
|
const uniqueModelName = generateUniqueAssetName({ baseName: obj.userData.modelName, existingAssets: assets, usedNames });
|
|
clone.userData.modelName = uniqueModelName;
|
|
usedNames.add(uniqueModelName);
|
|
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],
|
|
scale: [obj.scale.x, obj.scale.y, obj.scale.z],
|
|
isLocked: false,
|
|
isVisible: true,
|
|
isCollidable: false,
|
|
opacity: 0.5,
|
|
};
|
|
addAsset(asset);
|
|
});
|
|
}
|
|
}
|
|
}, [copiedObjects, pastedObjects.length, assets]);
|
|
|
|
const addPastedObjects = () => {
|
|
if (pastedObjects.length === 0) return;
|
|
|
|
const undoActions: UndoRedo3DAction[] = [];
|
|
const assetsToCopy: AssetData[] = [];
|
|
|
|
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: Asset = {
|
|
modelUuid: asset.modelUuid,
|
|
modelName: asset.modelName,
|
|
assetId: asset.assetId,
|
|
position: [position.x, position.y, position.z],
|
|
rotation: asset.rotation,
|
|
scale: asset.scale,
|
|
isCollidable: asset.isCollidable,
|
|
opacity: asset.opacity,
|
|
isLocked: asset.isLocked,
|
|
isVisible: asset.isVisible,
|
|
};
|
|
|
|
if (asset.eventData) {
|
|
let updatedEventData = JSON.parse(JSON.stringify(asset.eventData)) as EventsSchema;
|
|
updatedEventData.modelUuid = newFloorItem.modelUuid;
|
|
|
|
const eventData: any = {
|
|
type: asset.eventData.type,
|
|
subType: asset.eventData.subType || "",
|
|
};
|
|
|
|
let points: THREE.Vector3[] = [];
|
|
|
|
if ("point" in updatedEventData) {
|
|
points = [new THREE.Vector3(...updatedEventData.point.position)];
|
|
} else if ("points" in updatedEventData) {
|
|
points = updatedEventData.points.map((point) => new THREE.Vector3(...point.position));
|
|
}
|
|
|
|
const assetEvent = createEventData(newFloorItem, updatedEventData.type, updatedEventData.subType, points);
|
|
if (!assetEvent) return;
|
|
|
|
if ("point" in assetEvent) {
|
|
eventData.point = {
|
|
uuid: assetEvent.point.uuid,
|
|
position: assetEvent.point.position,
|
|
rotation: assetEvent.point.rotation,
|
|
};
|
|
} else if ("points" in assetEvent) {
|
|
eventData.points = assetEvent.points.map((point) => ({
|
|
uuid: point.uuid,
|
|
position: point.position,
|
|
rotation: point.rotation,
|
|
}));
|
|
}
|
|
|
|
newFloorItem.eventData = eventData;
|
|
|
|
const data = {
|
|
organization,
|
|
modelUuid: newFloorItem.modelUuid,
|
|
modelName: newFloorItem.modelName,
|
|
assetId: newFloorItem.assetId,
|
|
position: newFloorItem.position,
|
|
rotation: newFloorItem.rotation,
|
|
scale: newFloorItem.scale,
|
|
isLocked: newFloorItem.isLocked,
|
|
isCollidable: newFloorItem.isCollidable,
|
|
opacity: newFloorItem.opacity,
|
|
isVisible: newFloorItem.isVisible,
|
|
socketId: builderSocket?.id,
|
|
eventData: eventData,
|
|
versionId: selectedVersion?.versionId || "",
|
|
userId,
|
|
projectId,
|
|
};
|
|
|
|
removeAssetFromScene(data.modelUuid, () => {
|
|
if (!builderSocket?.connected) {
|
|
// REST
|
|
|
|
setAssetsApi({
|
|
modelUuid: newFloorItem.modelUuid,
|
|
modelName: newFloorItem.modelName,
|
|
assetId: newFloorItem.assetId,
|
|
position: newFloorItem.position,
|
|
rotation: newFloorItem.rotation,
|
|
scale: newFloorItem.scale,
|
|
isCollidable: newFloorItem.isCollidable,
|
|
opacity: newFloorItem.opacity,
|
|
isLocked: newFloorItem.isLocked,
|
|
isVisible: newFloorItem.isVisible,
|
|
eventData: eventData,
|
|
versionId: selectedVersion?.versionId || "",
|
|
projectId: projectId || "",
|
|
})
|
|
.then((data) => {
|
|
if (!data.message || !data.data) {
|
|
echo.error(`Error pasting asset: ${newFloorItem.modelUuid}`);
|
|
return;
|
|
}
|
|
if (data.message === "Model created 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,
|
|
isCollidable: data.data.isCollidable,
|
|
isVisible: data.data.isVisible,
|
|
opacity: data.data.opacity,
|
|
...(data.data.eventData ? { eventData: data.data.eventData } : {}),
|
|
};
|
|
|
|
updateAssetInScene(model, () => {
|
|
echo.info(`Pasted asset: ${model.modelName}`);
|
|
});
|
|
} else {
|
|
echo.error(`Error pasting asset: ${newFloorItem.modelUuid}`);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
echo.error(`Error pasting asset: ${newFloorItem.modelUuid}`);
|
|
clearSelection();
|
|
});
|
|
} else {
|
|
// SOCKET
|
|
|
|
builderSocket.emit("v1:model-asset:copy", data);
|
|
}
|
|
});
|
|
} else {
|
|
const data = {
|
|
organization,
|
|
modelUuid: newFloorItem.modelUuid,
|
|
modelName: newFloorItem.modelName,
|
|
assetId: newFloorItem.assetId,
|
|
position: newFloorItem.position,
|
|
rotation: newFloorItem.rotation,
|
|
scale: newFloorItem.scale,
|
|
isLocked: newFloorItem.isLocked,
|
|
isCollidable: newFloorItem.isCollidable,
|
|
opacity: newFloorItem.opacity,
|
|
isVisible: newFloorItem.isVisible,
|
|
socketId: builderSocket?.id,
|
|
versionId: selectedVersion?.versionId || "",
|
|
projectId,
|
|
userId,
|
|
};
|
|
|
|
removeAssetFromScene(data.modelUuid, () => {
|
|
if (!builderSocket?.connected) {
|
|
// REST
|
|
|
|
setAssetsApi({
|
|
modelUuid: newFloorItem.modelUuid,
|
|
modelName: newFloorItem.modelName,
|
|
assetId: newFloorItem.assetId,
|
|
position: newFloorItem.position,
|
|
rotation: newFloorItem.rotation,
|
|
scale: newFloorItem.scale,
|
|
isCollidable: newFloorItem.isCollidable,
|
|
opacity: newFloorItem.opacity,
|
|
isLocked: newFloorItem.isLocked,
|
|
isVisible: newFloorItem.isVisible,
|
|
versionId: selectedVersion?.versionId || "",
|
|
projectId: projectId || "",
|
|
})
|
|
.then((data) => {
|
|
if (!data.message || !data.data) {
|
|
echo.error(`Error pasting asset: ${newFloorItem.modelUuid}`);
|
|
return;
|
|
}
|
|
if (data.message === "Model created 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,
|
|
isCollidable: data.data.isCollidable,
|
|
isVisible: data.data.isVisible,
|
|
opacity: data.data.opacity,
|
|
...(data.data.eventData ? { eventData: data.data.eventData } : {}),
|
|
};
|
|
|
|
updateAssetInScene(model, () => {
|
|
echo.info(`Pasted asset: ${model.modelUuid}`);
|
|
});
|
|
} else {
|
|
echo.error(`Error pasting asset: ${newFloorItem.modelUuid}`);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
echo.error(`Error pasting asset: ${newFloorItem.modelUuid}`);
|
|
clearSelection();
|
|
});
|
|
} else {
|
|
// SOCKET
|
|
|
|
builderSocket.emit("v1:model-asset:copy", data);
|
|
}
|
|
});
|
|
}
|
|
|
|
assetsToCopy.push({
|
|
type: "Asset",
|
|
assetData: {
|
|
modelUuid: newFloorItem.modelUuid,
|
|
modelName: newFloorItem.modelName,
|
|
assetId: newFloorItem.assetId,
|
|
position: [position.x, position.y, position.z],
|
|
rotation: newFloorItem.rotation,
|
|
scale: newFloorItem.scale,
|
|
isCollidable: newFloorItem.isCollidable,
|
|
opacity: newFloorItem.opacity,
|
|
isLocked: newFloorItem.isLocked,
|
|
isVisible: newFloorItem.isVisible,
|
|
eventData: newFloorItem.eventData,
|
|
},
|
|
timeStap: new Date().toISOString(),
|
|
});
|
|
}
|
|
});
|
|
|
|
if (assetsToCopy.length === 1) {
|
|
undoActions.push({
|
|
module: "builder",
|
|
actionType: "Asset-Copied",
|
|
asset: assetsToCopy[0],
|
|
});
|
|
} else {
|
|
undoActions.push({
|
|
module: "builder",
|
|
actionType: "Assets-Copied",
|
|
assets: assetsToCopy,
|
|
});
|
|
}
|
|
|
|
push3D({
|
|
type: "Scene",
|
|
actions: undoActions,
|
|
});
|
|
|
|
clearSelection();
|
|
};
|
|
|
|
const clearSelection = () => {
|
|
setMovedObjects([]);
|
|
setPastedObjects([]);
|
|
setDuplicatedObjects([]);
|
|
setRotatedObjects([]);
|
|
setIsPasting(false);
|
|
setCenterOffset(null);
|
|
};
|
|
|
|
return null;
|
|
};
|
|
|
|
export default CutCopyPasteControls3D;
|