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) => { 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) => { 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) => { if (visible && !toggleView && activeModule === "builder") { if (e.object.userData.decalUuid) { e.stopPropagation(); if (toolMode === "3D-Delete") { setDeletableDecal(e.object); } } } }; const handlePointerLeave = (e: ThreeEvent) => { 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, }; }