Files
Dwinzo_Demo/app/src/modules/builder/Decal/eventHandler/useDecalEventHandlers.ts

559 lines
25 KiB
TypeScript

import { MathUtils, Vector2, Vector3 } from "three";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { CameraControls } from "@react-three/drei";
import { ThreeEvent, useFrame, useThree } from "@react-three/fiber";
import { useToggleView, useToolMode } from "../../../../store/builder/store";
import { useSocketStore } from "../../../../store/socket/useSocketStore";
import { useBuilderStore } from "../../../../store/builder/useBuilderStore";
import { useSceneContext } from "../../../scene/sceneContext";
import { useWallClassification } from "../../wall/Instances/instance/helpers/useWallClassification";
import useModuleStore from "../../../../store/ui/useModuleStore";
import useWallResponseHandler from "../../../collaboration/responseHandler/useWallResponseHandler";
import useFloorResponseHandler from "../../../collaboration/responseHandler/useFloorResponseHandler";
import { getUserData } from "../../../../functions/getUserData";
import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys";
import handleDecalPositionSnap from "../functions/handleDecalPositionSnap";
import { upsertWallApi } from "../../../../services/factoryBuilder/wall/upsertWallApi";
import { upsertFloorApi } from "../../../../services/factoryBuilder/floor/upsertFloorApi";
export function useDecalEventHandlers({ parent, decal, visible }: { parent: Wall | Floor; decal: Decal; visible: boolean }) {
const { wallStore, floorStore, versionStore } = useSceneContext();
const { walls, removeDecal: removeDecalInWall, updateDecalPosition: updateDecalPositionInWall, getWallById, addDecal: addDecalToWall, getDecalById: getDecalOnWall } = wallStore();
const { removeDecal: removeDecalInFloor, updateDecalPosition: updateDecalPositionInFloor, getFloorById, addDecal: addDecalToFloor, getDecalById: getDecalOnFloor } = floorStore();
const { setSelectedWall, setSelectedFloor, setSelectedDecal, setDeletableDecal, deletableDecal, selectedDecal, setDecalDragState, decalDragState } = useBuilderStore();
const { updateWallInScene } = useWallResponseHandler();
const { isWallFlipped } = useWallClassification(walls);
const { updateFloorInScene } = useFloorResponseHandler();
const { toolMode } = useToolMode();
const { toggleView } = useToggleView();
const { activeModule } = useModuleStore();
const { userId, organization } = getUserData();
const { selectedVersion } = versionStore();
const { projectId } = useParams();
const { builderSocket } = useSocketStore();
const [keyEvent, setKeyEvent] = useState<"Ctrl" | "">("");
const { raycaster, pointer, camera, scene, gl, controls } = useThree();
useFrame(() => {
if (activeModule !== "builder" || toggleView || !decalDragState.isDragging || !selectedDecal || selectedDecal.decalData.decalUuid !== decal.decalUuid) return;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
const wallIntersect = intersects.find((i) => i.object.userData?.wallUuid);
const floorIntersect = intersects.find((i) => i.object.userData?.floorUuid);
let offset = decalDragState.dragOffset || new Vector3(0, 0, 0);
if (wallIntersect) {
const wallUuid = wallIntersect.object.userData.wallUuid;
const point = wallIntersect.object.worldToLocal(wallIntersect.point.clone());
let finalPos;
if (keyEvent === "Ctrl") {
finalPos = handleDecalPositionSnap(point, offset, parent, decal, 0.05);
} else {
finalPos = point;
}
if ("wallUuid" in parent && parent.wallUuid === wallUuid && decal.decalType.type === "Wall") {
const wall = getWallById(wallUuid);
const decalData = getDecalOnWall(decal.decalUuid);
if (!decalData || !wall) return;
const wallFlipped = isWallFlipped(wall);
const [rawStart, rawEnd] = wall.points;
const [startPoint, endPoint] = wallFlipped ? [rawStart, rawEnd] : [rawEnd, rawStart];
const startX = startPoint.position[0];
const startZ = startPoint.position[2];
const endX = endPoint.position[0];
const endZ = endPoint.position[2];
const wallLength = Math.sqrt((endX - startX) ** 2 + (endZ - startZ) ** 2);
function clampDecalPosition(decal: Decal, wallLength: number, wallHeight: number) {
const localPos = new Vector3(...decal.decalPosition);
localPos.x = MathUtils.clamp(localPos.x, -wallLength / 2, wallLength / 2);
localPos.y = MathUtils.clamp(localPos.y, -wallHeight / 2, wallHeight / 2);
return localPos.toArray() as [number, number, number];
}
const clampedPosition = clampDecalPosition({ ...decalData, decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]] }, wallLength, wall.wallHeight);
updateDecalPositionInWall(decalData.decalUuid, clampedPosition);
} else if (decal.decalType.type === "Wall" && wallUuid) {
deleteDecal(decal.decalUuid, parent);
const addedDecal = addDecalToWall(wallUuid, {
...decal,
decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]],
decalType: { type: "Wall", wallUuid: wallUuid },
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal });
}
} else if (decal.decalType.type === "Floor" && wallUuid) {
deleteDecal(decal.decalUuid, parent);
const wall = getWallById(wallUuid);
if (!wall) return;
const addedDecal = addDecalToWall(wallUuid, {
...decal,
decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, wall.wallThickness / 2 + 0.001],
decalType: { type: "Wall", wallUuid: wallUuid },
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal });
}
}
} else if (floorIntersect) {
const floorUuid = floorIntersect.object.userData.floorUuid;
const point = floorIntersect.object.worldToLocal(floorIntersect.point.clone());
let finalPos;
if (keyEvent === "Ctrl") {
finalPos = handleDecalPositionSnap(point, offset, parent, decal, 0.25);
} else {
finalPos = point;
}
if ("floorUuid" in parent && parent.floorUuid === floorUuid && decal.decalType.type === "Floor") {
function isPointInPolygon(point: Vector2, polygon: Vector2[]): boolean {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x,
yi = polygon[i].y;
const xj = polygon[j].x,
yj = polygon[j].y;
const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
function clampToPolygon(point: Vector2, polygon: Vector2[]): Vector2 {
let closestPoint = point.clone();
let minDist = Infinity;
for (let i = 0; i < polygon.length; i++) {
const a = polygon[i];
const b = polygon[(i + 1) % polygon.length];
const ab = new Vector2().subVectors(b, a);
const t = Math.max(0, Math.min(1, point.clone().sub(a).dot(ab) / ab.lengthSq()));
const proj = a.clone().add(ab.multiplyScalar(t));
const dist = proj.distanceTo(point);
if (dist < minDist) {
minDist = dist;
closestPoint = proj;
}
}
return closestPoint;
}
function clampDecalPosition(decal: Decal, floor: Floor): [number, number, number] {
const pos = new Vector3(...decal.decalPosition);
const polygon2D = floor.points.map((p) => new Vector2(p.position[0], p.position[2]));
const p2 = new Vector2(pos.x, pos.y);
let final2D: Vector2;
if (isPointInPolygon(p2, polygon2D)) {
final2D = p2;
} else {
final2D = clampToPolygon(p2, polygon2D);
}
const clampedPos = new Vector3(final2D.x, final2D.y, pos.z);
return clampedPos.toArray() as [number, number, number];
}
const clampedPosition = clampDecalPosition({ ...decal, decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]] }, parent);
updateDecalPositionInFloor(decal.decalUuid, clampedPosition);
} else if (decal.decalType.type === "Floor" && floorUuid) {
deleteDecal(decal.decalUuid, parent);
const addedDecal = addDecalToFloor(floorUuid, {
...decal,
decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, decal.decalPosition[2]],
decalType: { type: "Floor", floorUuid: floorUuid },
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal });
}
} else if (decal.decalType.type === "Wall" && floorUuid) {
deleteDecal(decal.decalUuid, parent);
const floor = getFloorById(floorUuid);
if (!floor) return;
const addedDecal = addDecalToFloor(floorUuid, {
...decal,
decalPosition: [finalPos.x + offset.x, finalPos.y + offset.y, -0.001],
decalType: { type: "Floor", floorUuid: floorUuid },
});
if (addedDecal) {
setSelectedDecal({ decalMesh: null, decalData: addedDecal });
}
}
}
});
const handlePointerUp = () => {
if (controls) {
(controls as CameraControls).enabled = true;
}
if (decalDragState.isDragging) {
setDecalDragState(false, null, null);
if ("wallUuid" in parent) {
setTimeout(() => {
const updatedWall = getWallById(parent.wallUuid);
if (updatedWall) {
if (projectId) {
if (!builderSocket?.connected) {
// API
upsertWallApi(projectId, selectedVersion?.versionId || "", updatedWall)
.then((data) => {
if (!data.message || !data.data) {
echo.error(`Error updating decal`);
return;
}
if (data.message === "Wall Updated Successfully") {
updateWallInScene(updatedWall, () => {
echo.info(`Decal Updated`);
});
} else {
echo.error(`Error updating decal`);
}
})
.catch(() => {
echo.error(`Error updating decal`);
});
} else {
// SOCKET
const data = {
wallData: updatedWall,
projectId: projectId,
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
};
builderSocket.emit("v1:model-Wall:add", data);
}
}
}
}, 0);
} else if ("floorUuid" in parent) {
setTimeout(() => {
const updatedFloor = parent;
if (projectId && updatedFloor) {
if (!builderSocket?.connected) {
// API
upsertFloorApi(projectId, selectedVersion?.versionId || "", updatedFloor)
.then((data) => {
if (!data.message || !data.data) {
echo.error(`Error updating decal`);
return;
}
if (data.message === "Floor Updated Successfully") {
updateFloorInScene(updatedFloor, () => {
echo.info(`Decal Updated`);
});
} else {
echo.error(`Error updating decal`);
}
})
.catch(() => {
echo.error(`Error updating decal`);
});
} else {
// SOCKET
const data = {
floorData: updatedFloor,
projectId: projectId,
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
};
builderSocket.emit("v1:model-Floor:add", data);
}
}
}, 0);
}
}
};
const deleteDecal = (decalUuid: string, parent: Wall | Floor) => {
if ("wallUuid" in parent) {
const updatedWall = removeDecalInWall(decalUuid);
if (projectId && updatedWall) {
if (!builderSocket?.connected) {
// API
upsertWallApi(projectId, selectedVersion?.versionId || "", updatedWall)
.then((data) => {
if (!data.message || !data.data) {
echo.error(`Error removing decal`);
return;
}
if (data.message === "Wall Updated Successfully") {
updateWallInScene(updatedWall, () => {
echo.info(`Decal Removed`);
});
} else {
echo.error(`Error removing decal`);
}
})
.catch(() => {
echo.error(`Error removing decal`);
});
} else {
// SOCKET
const data = {
wallData: updatedWall,
projectId: projectId,
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
};
builderSocket.emit("v1:model-Wall:add", data);
}
}
} else if ("floorUuid" in parent) {
const updatedFloor = removeDecalInFloor(decalUuid);
if (projectId && updatedFloor) {
if (!builderSocket?.connected) {
// API
upsertFloorApi(projectId, selectedVersion?.versionId || "", updatedFloor);
} else {
// SOCKET
const data = {
floorData: updatedFloor,
projectId: projectId,
versionId: selectedVersion?.versionId || "",
userId: userId,
organization: organization,
};
builderSocket.emit("v1:model-Floor:add", data);
}
}
}
};
const handlePointerDown = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === "builder") {
if (e.object.userData.decalUuid && toolMode === "cursor" && selectedDecal && selectedDecal.decalData.decalUuid === decal.decalUuid) {
e.stopPropagation();
setDecalDragState(true, decal.decalUuid, null);
if (controls) {
(controls as CameraControls).enabled = false;
}
setSelectedWall(null);
setSelectedFloor(null);
const localIntersect = e.object.worldToLocal(e.point.clone());
let clampedDecalPosition;
if (decal.decalType.type === "Wall" && "wallUuid" in parent) {
const wallFlipped = isWallFlipped(parent);
const [rawStart, rawEnd] = parent.points;
const [startPoint, endPoint] = wallFlipped ? [rawStart, rawEnd] : [rawEnd, rawStart];
const startX = startPoint.position[0];
const startZ = startPoint.position[2];
const endX = endPoint.position[0];
const endZ = endPoint.position[2];
const wallLength = Math.sqrt((endX - startX) ** 2 + (endZ - startZ) ** 2);
function clampDecalPosition(decal: Decal, wallLength: number, wallHeight: number) {
const localPos = new Vector3(...decal.decalPosition);
localPos.x = MathUtils.clamp(localPos.x, -wallLength / 2, wallLength / 2);
localPos.y = MathUtils.clamp(localPos.y, -wallHeight / 2, wallHeight / 2);
return localPos.toArray() as [number, number, number];
}
clampedDecalPosition = clampDecalPosition(decal, wallLength, parent.wallHeight);
} else if (decal.decalType.type === "Floor" && "floorUuid" in parent) {
function isPointInPolygon(point: Vector2, polygon: Vector2[]): boolean {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i].x,
yi = polygon[i].y;
const xj = polygon[j].x,
yj = polygon[j].y;
const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
function clampToPolygon(point: Vector2, polygon: Vector2[]): Vector2 {
let closestPoint = point.clone();
let minDist = Infinity;
for (let i = 0; i < polygon.length; i++) {
const a = polygon[i];
const b = polygon[(i + 1) % polygon.length];
const ab = new Vector2().subVectors(b, a);
const t = Math.max(0, Math.min(1, point.clone().sub(a).dot(ab) / ab.lengthSq()));
const proj = a.clone().add(ab.multiplyScalar(t));
const dist = proj.distanceTo(point);
if (dist < minDist) {
minDist = dist;
closestPoint = proj;
}
}
return closestPoint;
}
function clampDecalPosition(decal: Decal, floor: Floor): [number, number, number] {
const pos = new Vector3(...decal.decalPosition);
const polygon2D = floor.points.map((p) => new Vector2(p.position[0], p.position[2]));
const p2 = new Vector2(pos.x, pos.y);
let final2D: Vector2;
if (isPointInPolygon(p2, polygon2D)) {
final2D = p2;
} else {
final2D = clampToPolygon(p2, polygon2D);
}
const clampedPos = new Vector3(final2D.x, final2D.y, pos.z);
return clampedPos.toArray() as [number, number, number];
}
clampedDecalPosition = clampDecalPosition(decal, parent);
} else {
clampedDecalPosition = decal.decalPosition;
}
let dragOffset = new Vector3(clampedDecalPosition[0] - localIntersect.x, clampedDecalPosition[1] - localIntersect.y, 0);
setDecalDragState(true, decal.decalUuid, dragOffset);
}
}
};
const handleClick = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === "builder") {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === "cursor") {
setSelectedDecal({ decalMesh: e.object, decalData: decal });
setSelectedWall(null);
setSelectedFloor(null);
} else if (toolMode === "3D-Delete") {
deleteDecal(e.object.userData.decalUuid, parent);
}
}
}
};
const handlePointerEnter = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === "builder") {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === "3D-Delete") {
setDeletableDecal(e.object);
}
}
}
};
const handlePointerLeave = (e: ThreeEvent<MouseEvent>) => {
if (visible && !toggleView && activeModule === "builder") {
if (e.object.userData.decalUuid) {
e.stopPropagation();
if (toolMode === "3D-Delete" && deletableDecal && deletableDecal?.userData.decalUuid === e.object.userData.decalUuid) {
setDeletableDecal(null);
}
}
}
};
const handlePointerMissed = () => {
if (selectedDecal?.decalMesh && selectedDecal.decalMesh.userData.decalUuid === decal.decalUuid) {
setSelectedDecal(null);
setKeyEvent("");
}
};
const onKeyUp = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (keyCombination === "") {
setKeyEvent("");
} else if (keyCombination === "Ctrl") {
setKeyEvent(keyCombination);
}
};
const onKeyDown = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (keyCombination !== keyEvent) {
if (keyCombination === "Ctrl") {
setKeyEvent(keyCombination);
} else {
setKeyEvent("");
}
}
};
useEffect(() => {
const canvasElement = gl.domElement;
if (activeModule === "builder" && !toggleView && selectedDecal && selectedDecal.decalData.decalUuid === decal.decalUuid) {
canvasElement.addEventListener("pointerup", handlePointerUp);
canvasElement?.addEventListener("keyup", onKeyUp);
canvasElement.addEventListener("keydown", onKeyDown);
} else {
setKeyEvent("");
}
return () => {
canvasElement.removeEventListener("pointerup", handlePointerUp);
canvasElement?.removeEventListener("keyup", onKeyUp);
canvasElement.removeEventListener("keydown", onKeyDown);
};
// eslint-disable-next-line
}, [gl, activeModule, toggleView, selectedDecal, camera, controls, visible, parent, decal, decalDragState]);
return {
handlePointerDown,
handleClick,
handlePointerEnter,
handlePointerLeave,
handlePointerMissed,
deleteDecal,
};
}