From 8b7c28e9c00b6c96eb3caa6e9118792219ae8969 Mon Sep 17 00:00:00 2001 From: Vishnu Date: Wed, 7 May 2025 15:31:07 +0530 Subject: [PATCH] Enhance collaboration features by adding user targeting and camera control functionality - Implement user following functionality in Header component - Update FollowPerson component to handle target and rotation - Modify CollabUserIcon to include target data for selected users - Adjust setCameraView function to utilize target for camera positioning - Extend user types to include position, rotation, and target properties --- .../components/layout/sidebarRight/Header.tsx | 44 +- app/src/components/templates/FollowPerson.tsx | 4 +- .../collaboration/camera/collabCams.tsx | 424 +++++++++--------- .../collaboration/camera/collabUserIcon.tsx | 15 +- .../modules/collaboration/collaboration.tsx | 20 +- .../collaboration/functions/setCameraView.ts | 9 +- app/src/store/useCollabStore.ts | 7 +- app/src/types/users.d.ts | 3 + 8 files changed, 303 insertions(+), 223 deletions(-) diff --git a/app/src/components/layout/sidebarRight/Header.tsx b/app/src/components/layout/sidebarRight/Header.tsx index a88baee..02917b4 100644 --- a/app/src/components/layout/sidebarRight/Header.tsx +++ b/app/src/components/layout/sidebarRight/Header.tsx @@ -1,10 +1,10 @@ import React, { useState } from "react"; -import { AppDockIcon } from "../../icons/HeaderIcons"; import orgImg from "../../../assets/orgTemp.png"; -import { useActiveUsers } from "../../../store/store"; +import { useActiveUsers, useCamMode } from "../../../store/store"; import { ActiveUser } from "../../../types/users"; import CollaborationPopup from "../../templates/CollaborationPopup"; import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor"; +import { useSelectedUserStore } from "../../../store/useCollabStore"; const Header: React.FC = () => { const { activeUsers } = useActiveUsers(); @@ -15,6 +15,37 @@ const Header: React.FC = () => { ); const [userManagement, setUserManagement] = useState(false); + const { setSelectedUser } = useSelectedUserStore(); + const { setCamMode } = useCamMode(); + + function handleUserFollow(user: any, index: number) { + const position = { + x: user.position?.x!, + y: user.position?.y!, + z: user.position?.z!, + }; + const target = { + x: user.target?.x!, + y: user.target?.y!, + z: user.target?.z!, + }; + const rotation = { + x: user.rotation?.x!, + y: user.rotation?.y!, + z: user.rotation?.z!, + }; + + // retun on no data + if (!position || !target || !rotation) return; + + // Set the selected user in the store + setSelectedUser({ + color: getAvatarColor(index, user.userName), + name: user.userName, + location: { position, rotation, target }, + }); + setCamMode("FollowPerson"); + } return ( <> @@ -42,13 +73,16 @@ const Header: React.FC = () => {
+{guestUsers.length - 3}
)} {guestUsers.slice(0, 3).map((user, index) => ( -
{ + handleUserFollow(user, index); + }} > {user.userName[0]} -
+ ))}
diff --git a/app/src/components/templates/FollowPerson.tsx b/app/src/components/templates/FollowPerson.tsx index a85d0c9..832c7f1 100644 --- a/app/src/components/templates/FollowPerson.tsx +++ b/app/src/components/templates/FollowPerson.tsx @@ -13,13 +13,13 @@ const FollowPerson: React.FC = () => { // eslint-disable-next-line
{ + onPointerDown={() => { clearSelectedUser(); setCamMode("FirstPerson"); }} style={{ "--user-color": selectedUser.color } as React.CSSProperties} > -
{selectedUser.name}
+
Viewing through {selectedUser.name}’s eyes
)} diff --git a/app/src/modules/collaboration/camera/collabCams.tsx b/app/src/modules/collaboration/camera/collabCams.tsx index 461cb7f..a3e4eb4 100644 --- a/app/src/modules/collaboration/camera/collabCams.tsx +++ b/app/src/modules/collaboration/camera/collabCams.tsx @@ -11,230 +11,254 @@ import { Html } from "@react-three/drei"; import CollabUserIcon from "./collabUserIcon"; import useModuleStore from "../../../store/useModuleStore"; import { getAvatarColor } from "../functions/getAvatarColor"; +import { useSelectedUserStore } from "../../../store/useCollabStore"; const CamModelsGroup = () => { - const navigate = useNavigate(); - const groupRef = useRef(null); - const email = localStorage.getItem("email"); - const { setActiveUsers } = useActiveUsers(); - const { socket } = useSocketStore(); - const { activeModule } = useModuleStore(); + const navigate = useNavigate(); + const groupRef = useRef(null); + const email = localStorage.getItem("email"); + const { setActiveUsers } = useActiveUsers(); + const { socket } = useSocketStore(); + const { activeModule } = useModuleStore(); + const { selectedUser } = useSelectedUserStore(); - // eslint-disable-next-line react-hooks/exhaustive-deps - const loader = new GLTFLoader(); - const dracoLoader = new DRACOLoader(); - dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/"); - loader.setDRACOLoader(dracoLoader); + // eslint-disable-next-line react-hooks/exhaustive-deps + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); + dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/"); + loader.setDRACOLoader(dracoLoader); - const [cams, setCams] = useState([]); - const [models, setModels] = useState>({}); + const [cams, setCams] = useState([]); + const [models, setModels] = useState< + Record< + string, + { + targetPosition: THREE.Vector3; + targetRotation: THREE.Euler; + target: THREE.Vector3; + } + > + >({}); - const dedupeCams = (cams: any[]) => { - const seen = new Set(); - return cams.filter((cam) => { - if (seen.has(cam.uuid)) return false; - seen.add(cam.uuid); - return true; - }); - }; + const dedupeCams = (cams: any[]) => { + const seen = new Set(); + return cams.filter((cam) => { + if (seen.has(cam.uuid)) return false; + seen.add(cam.uuid); + return true; + }); + }; - const dedupeUsers = (users: any[]) => { - const seen = new Set(); - return users.filter((user) => { - if (seen.has(user._id)) return false; - seen.add(user._id); - return true; - }); - }; + const dedupeUsers = (users: any[]) => { + const seen = new Set(); + return users.filter((user) => { + if (seen.has(user._id)) return false; + seen.add(user._id); + return true; + }); + }; - useEffect(() => { - if (!email) navigate("/"); + useEffect(() => { + if (!email) navigate("/"); - if (!socket) return; - const organization = email!.split("@")[1].split(".")[0]; + if (!socket) return; + const organization = email!.split("@")[1].split(".")[0]; - socket.on("userConnectResponse", (data: any) => { - if (!groupRef.current) return; - if (data.data.userData.email === email) return; - if (socket.id === data.socketId || organization !== data.organization) - return; + socket.on("userConnectResponse", (data: any) => { + if (!groupRef.current) return; + if (data.data.userData.email === email) return; + if (socket.id === data.socketId || organization !== data.organization) + return; - const model = groupRef.current.getObjectByProperty( - "uuid", - data.data.userData._id - ); - if (model) { - groupRef.current.remove(model); - } + const model = groupRef.current.getObjectByProperty( + "uuid", + data.data.userData._id + ); + if (model) { + groupRef.current.remove(model); + } - loader.load(camModel, (gltf) => { - const newModel = gltf.scene.clone(); - newModel.uuid = data.data.userData._id; - newModel.position.set( - data.data.position.x, - data.data.position.y, - data.data.position.z - ); - newModel.rotation.set( - data.data.rotation.x, - data.data.rotation.y, - data.data.rotation.z - ); - newModel.userData = data.data.userData; + loader.load(camModel, (gltf) => { + const newModel = gltf.scene.clone(); + newModel.uuid = data.data.userData._id; + newModel.position.set( + data.data.position.x, + data.data.position.y, + data.data.position.z + ); + newModel.rotation.set( + data.data.rotation.x, + data.data.rotation.y, + data.data.rotation.z + ); + newModel.userData = data.data.userData; + newModel.userData.target = new THREE.Vector3( + data.data.target.x, + data.data.target.y, + data.data.target.z + ); - setCams((prev) => dedupeCams([...prev, newModel])); - setActiveUsers((prev: any) => - dedupeUsers([...prev, data.data.userData]) - ); - }); - }); + setCams((prev) => dedupeCams([...prev, newModel])); + setActiveUsers((prev: any) => + dedupeUsers([...prev, data.data.userData]) + ); + }); + }); - socket.on("userDisConnectResponse", (data: any) => { - if (!groupRef.current) return; - if (socket.id === data.socketId || organization !== data.organization) - return; + socket.on("userDisConnectResponse", (data: any) => { + if (!groupRef.current) return; + if (socket.id === data.socketId || organization !== data.organization) + return; - setCams((prev) => - prev.filter((cam) => cam.uuid !== data.data.userData._id) - ); - setActiveUsers((prev: any) => - prev.filter((user: any) => user._id !== data.data.userData._id) - ); - }); + setCams((prev) => + prev.filter((cam) => cam.uuid !== data.data.userData._id) + ); + setActiveUsers((prev: any) => + prev.filter((user: any) => user._id !== data.data.userData._id) + ); + }); - socket.on("cameraUpdateResponse", (data: any) => { - - if ( - !groupRef.current || - socket.id === data.socketId || - organization !== data.organization - ) - return; + socket.on("cameraUpdateResponse", (data: any) => { + if ( + !groupRef.current || + socket.id === data.socketId || + organization !== data.organization + ) + return; - setModels((prev) => ({ - ...prev, - [data.data.userId]: { - targetPosition: new THREE.Vector3( - data.data.position.x, - data.data.position.y, - data.data.position.z - ), - targetRotation: new THREE.Euler( - data.data.rotation.x, - data.data.rotation.y, - data.data.rotation.z - ), - target: new THREE.Vector3( - data.data.target.x, - data.data.target.y, - data.data.target.z - ), - }, - })); - }); + setModels((prev) => ({ + ...prev, + [data.data.userId]: { + targetPosition: new THREE.Vector3( + data.data.position.x, + data.data.position.y, + data.data.position.z + ), + targetRotation: new THREE.Euler( + data.data.rotation.x, + data.data.rotation.y, + data.data.rotation.z + ), + target: new THREE.Vector3( + data.data.target.x, + data.data.target.y, + data.data.target.z + ), + }, + })); + }); - return () => { - socket.off("userConnectResponse"); - socket.off("userDisConnectResponse"); - socket.off("cameraUpdateResponse"); - }; - }, [email, loader, navigate, setActiveUsers, socket]); + return () => { + socket.off("userConnectResponse"); + socket.off("userDisConnectResponse"); + socket.off("cameraUpdateResponse"); + }; + }, [email, loader, navigate, setActiveUsers, socket]); - useFrame(() => { - if (!groupRef.current) return; - Object.keys(models).forEach((uuid) => { - const model = groupRef.current!.getObjectByProperty("uuid", uuid); - if (!model) return; + useFrame(() => { + if (!groupRef.current) return; + Object.keys(models).forEach((uuid) => { + const model = groupRef.current!.getObjectByProperty("uuid", uuid); + if (!model) return; - const { targetPosition, targetRotation } = models[uuid]; - model.position.lerp(targetPosition, 0.1); - model.rotation.x = THREE.MathUtils.lerp( - model.rotation.x, - targetRotation.x, - 0.1 - ); - model.rotation.y = THREE.MathUtils.lerp( - model.rotation.y, - targetRotation.y, - 0.1 - ); - model.rotation.z = THREE.MathUtils.lerp( - model.rotation.z, - targetRotation.z, - 0.1 - ); - }); - }); + const { targetPosition, targetRotation } = models[uuid]; + model.position.lerp(targetPosition, 0.1); + model.rotation.x = THREE.MathUtils.lerp( + model.rotation.x, + targetRotation.x, + 0.1 + ); + model.rotation.y = THREE.MathUtils.lerp( + model.rotation.y, + targetRotation.y, + 0.1 + ); + model.rotation.z = THREE.MathUtils.lerp( + model.rotation.z, + targetRotation.z, + 0.1 + ); + }); + }); - useEffect(() => { - if (!groupRef.current) return; - const organization = email!.split("@")[1].split(".")[0]; + useEffect(() => { + if (!groupRef.current) return; + const organization = email!.split("@")[1].split(".")[0]; - getActiveUsersData(organization).then((data) => { - const filteredData = data.cameraDatas.filter( - (camera: any) => camera.userData.email !== email - ); + getActiveUsersData(organization).then((data) => { + const filteredData = data.cameraDatas.filter( + (camera: any) => camera.userData.email !== email + ); - if (filteredData.length > 0) { - loader.load(camModel, (gltf) => { - const newCams = filteredData.map((cam: any) => { - const newModel = gltf.scene.clone(); - newModel.uuid = cam.userData._id; - newModel.position.set( - cam.position.x, - cam.position.y, - cam.position.z - ); - newModel.rotation.set( - cam.rotation.x, - cam.rotation.y, - cam.rotation.z - ); - newModel.userData = cam.userData; - return newModel; - }); + if (filteredData.length > 0) { + loader.load(camModel, (gltf) => { + const newCams = filteredData.map((cam: any) => { + const newModel = gltf.scene.clone(); + newModel.uuid = cam.userData._id; + newModel.position.set( + cam.position.x, + cam.position.y, + cam.position.z + ); + newModel.rotation.set( + cam.rotation.x, + cam.rotation.y, + cam.rotation.z + ); + newModel.userData = cam.userData; + cam.userData.position = newModel.position; + cam.userData.rotation = newModel.rotation; + newModel.userData.target = cam.target; - const users = filteredData.map((cam: any) => cam.userData); - setActiveUsers((prev: any) => dedupeUsers([...prev, ...users])); - setCams((prev) => dedupeCams([...prev, ...newCams])); - }); - } - }); - }, []); + return newModel; + }); - return ( - - {cams.map((cam, index) => ( - - - - - - ))} - - ); + const users = filteredData.map((cam: any) => cam.userData); + setActiveUsers((prev: any) => dedupeUsers([...prev, ...users])); + setCams((prev) => dedupeCams([...prev, ...newCams])); + }); + } + }); + }, []); + + return ( + + {cams.map((cam, index) => ( + + + + + + ))} + + ); }; export default CamModelsGroup; diff --git a/app/src/modules/collaboration/camera/collabUserIcon.tsx b/app/src/modules/collaboration/camera/collabUserIcon.tsx index dcdb73b..885c06f 100644 --- a/app/src/modules/collaboration/camera/collabUserIcon.tsx +++ b/app/src/modules/collaboration/camera/collabUserIcon.tsx @@ -17,6 +17,11 @@ interface CollabUserIconProps { y: number; z: number; }; + target?: { + x: number; + y: number; + z: number; + }; } const CollabUserIcon: React.FC = ({ @@ -25,6 +30,7 @@ const CollabUserIcon: React.FC = ({ color, position, rotation, + target, }) => { const { setSelectedUser } = useSelectedUserStore(); const { setCamMode } = useCamMode(); @@ -33,10 +39,13 @@ const CollabUserIcon: React.FC = ({