559 lines
25 KiB
TypeScript
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,
|
|
};
|
|
}
|