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
This commit is contained in:
2025-05-07 15:31:07 +05:30
parent ad2b6b96f3
commit 8b7c28e9c0
8 changed files with 303 additions and 223 deletions

View File

@@ -1,10 +1,10 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { AppDockIcon } from "../../icons/HeaderIcons";
import orgImg from "../../../assets/orgTemp.png"; import orgImg from "../../../assets/orgTemp.png";
import { useActiveUsers } from "../../../store/store"; import { useActiveUsers, useCamMode } from "../../../store/store";
import { ActiveUser } from "../../../types/users"; import { ActiveUser } from "../../../types/users";
import CollaborationPopup from "../../templates/CollaborationPopup"; import CollaborationPopup from "../../templates/CollaborationPopup";
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor"; import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
import { useSelectedUserStore } from "../../../store/useCollabStore";
const Header: React.FC = () => { const Header: React.FC = () => {
const { activeUsers } = useActiveUsers(); const { activeUsers } = useActiveUsers();
@@ -15,6 +15,37 @@ const Header: React.FC = () => {
); );
const [userManagement, setUserManagement] = useState(false); 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 ( return (
<> <>
@@ -42,13 +73,16 @@ const Header: React.FC = () => {
<div className="other-guest">+{guestUsers.length - 3}</div> <div className="other-guest">+{guestUsers.length - 3}</div>
)} )}
{guestUsers.slice(0, 3).map((user, index) => ( {guestUsers.slice(0, 3).map((user, index) => (
<div <button
key={index} key={`${index}-${user.userName}`}
className="user-profile" className="user-profile"
style={{ background: getAvatarColor(index, user.userName) }} style={{ background: getAvatarColor(index, user.userName) }}
onClick={() => {
handleUserFollow(user, index);
}}
> >
{user.userName[0]} {user.userName[0]}
</div> </button>
))} ))}
</div> </div>
<div className="user-profile-container"> <div className="user-profile-container">

View File

@@ -13,13 +13,13 @@ const FollowPerson: React.FC = () => {
// eslint-disable-next-line // eslint-disable-next-line
<div <div
className="follow-person-container" className="follow-person-container"
onClick={() => { onPointerDown={() => {
clearSelectedUser(); clearSelectedUser();
setCamMode("FirstPerson"); setCamMode("FirstPerson");
}} }}
style={{ "--user-color": selectedUser.color } as React.CSSProperties} style={{ "--user-color": selectedUser.color } as React.CSSProperties}
> >
<div className="follower-name">{selectedUser.name}</div> <div className="follower-name">Viewing through {selectedUser.name}s eyes</div>
</div> </div>
)} )}
</RenderOverlay> </RenderOverlay>

View File

@@ -11,230 +11,254 @@ import { Html } from "@react-three/drei";
import CollabUserIcon from "./collabUserIcon"; import CollabUserIcon from "./collabUserIcon";
import useModuleStore from "../../../store/useModuleStore"; import useModuleStore from "../../../store/useModuleStore";
import { getAvatarColor } from "../functions/getAvatarColor"; import { getAvatarColor } from "../functions/getAvatarColor";
import { useSelectedUserStore } from "../../../store/useCollabStore";
const CamModelsGroup = () => { const CamModelsGroup = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const email = localStorage.getItem("email"); const email = localStorage.getItem("email");
const { setActiveUsers } = useActiveUsers(); const { setActiveUsers } = useActiveUsers();
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { activeModule } = useModuleStore(); const { activeModule } = useModuleStore();
const { selectedUser } = useSelectedUserStore();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const loader = new GLTFLoader(); const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader(); const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/"); dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
loader.setDRACOLoader(dracoLoader); loader.setDRACOLoader(dracoLoader);
const [cams, setCams] = useState<any[]>([]); const [cams, setCams] = useState<any[]>([]);
const [models, setModels] = useState<Record<string, { targetPosition: THREE.Vector3; targetRotation: THREE.Euler, target: THREE.Vector3 }>>({}); const [models, setModels] = useState<
Record<
string,
{
targetPosition: THREE.Vector3;
targetRotation: THREE.Euler;
target: THREE.Vector3;
}
>
>({});
const dedupeCams = (cams: any[]) => { const dedupeCams = (cams: any[]) => {
const seen = new Set(); const seen = new Set();
return cams.filter((cam) => { return cams.filter((cam) => {
if (seen.has(cam.uuid)) return false; if (seen.has(cam.uuid)) return false;
seen.add(cam.uuid); seen.add(cam.uuid);
return true; return true;
}); });
}; };
const dedupeUsers = (users: any[]) => { const dedupeUsers = (users: any[]) => {
const seen = new Set(); const seen = new Set();
return users.filter((user) => { return users.filter((user) => {
if (seen.has(user._id)) return false; if (seen.has(user._id)) return false;
seen.add(user._id); seen.add(user._id);
return true; return true;
}); });
}; };
useEffect(() => { useEffect(() => {
if (!email) navigate("/"); if (!email) navigate("/");
if (!socket) return; if (!socket) return;
const organization = email!.split("@")[1].split(".")[0]; const organization = email!.split("@")[1].split(".")[0];
socket.on("userConnectResponse", (data: any) => { socket.on("userConnectResponse", (data: any) => {
if (!groupRef.current) return; if (!groupRef.current) return;
if (data.data.userData.email === email) return; if (data.data.userData.email === email) return;
if (socket.id === data.socketId || organization !== data.organization) if (socket.id === data.socketId || organization !== data.organization)
return; return;
const model = groupRef.current.getObjectByProperty( const model = groupRef.current.getObjectByProperty(
"uuid", "uuid",
data.data.userData._id data.data.userData._id
); );
if (model) { if (model) {
groupRef.current.remove(model); groupRef.current.remove(model);
} }
loader.load(camModel, (gltf) => { loader.load(camModel, (gltf) => {
const newModel = gltf.scene.clone(); const newModel = gltf.scene.clone();
newModel.uuid = data.data.userData._id; newModel.uuid = data.data.userData._id;
newModel.position.set( newModel.position.set(
data.data.position.x, data.data.position.x,
data.data.position.y, data.data.position.y,
data.data.position.z data.data.position.z
); );
newModel.rotation.set( newModel.rotation.set(
data.data.rotation.x, data.data.rotation.x,
data.data.rotation.y, data.data.rotation.y,
data.data.rotation.z data.data.rotation.z
); );
newModel.userData = data.data.userData; 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])); setCams((prev) => dedupeCams([...prev, newModel]));
setActiveUsers((prev: any) => setActiveUsers((prev: any) =>
dedupeUsers([...prev, data.data.userData]) dedupeUsers([...prev, data.data.userData])
); );
}); });
}); });
socket.on("userDisConnectResponse", (data: any) => { socket.on("userDisConnectResponse", (data: any) => {
if (!groupRef.current) return; if (!groupRef.current) return;
if (socket.id === data.socketId || organization !== data.organization) if (socket.id === data.socketId || organization !== data.organization)
return; return;
setCams((prev) => setCams((prev) =>
prev.filter((cam) => cam.uuid !== data.data.userData._id) prev.filter((cam) => cam.uuid !== data.data.userData._id)
); );
setActiveUsers((prev: any) => setActiveUsers((prev: any) =>
prev.filter((user: any) => user._id !== data.data.userData._id) prev.filter((user: any) => user._id !== data.data.userData._id)
); );
}); });
socket.on("cameraUpdateResponse", (data: any) => { socket.on("cameraUpdateResponse", (data: any) => {
if (
!groupRef.current ||
socket.id === data.socketId ||
organization !== data.organization
)
return;
if ( setModels((prev) => ({
!groupRef.current || ...prev,
socket.id === data.socketId || [data.data.userId]: {
organization !== data.organization targetPosition: new THREE.Vector3(
) data.data.position.x,
return; 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) => ({ return () => {
...prev, socket.off("userConnectResponse");
[data.data.userId]: { socket.off("userDisConnectResponse");
targetPosition: new THREE.Vector3( socket.off("cameraUpdateResponse");
data.data.position.x, };
data.data.position.y, }, [email, loader, navigate, setActiveUsers, socket]);
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 () => { useFrame(() => {
socket.off("userConnectResponse"); if (!groupRef.current) return;
socket.off("userDisConnectResponse"); Object.keys(models).forEach((uuid) => {
socket.off("cameraUpdateResponse"); const model = groupRef.current!.getObjectByProperty("uuid", uuid);
}; if (!model) return;
}, [email, loader, navigate, setActiveUsers, socket]);
useFrame(() => { const { targetPosition, targetRotation } = models[uuid];
if (!groupRef.current) return; model.position.lerp(targetPosition, 0.1);
Object.keys(models).forEach((uuid) => { model.rotation.x = THREE.MathUtils.lerp(
const model = groupRef.current!.getObjectByProperty("uuid", uuid); model.rotation.x,
if (!model) return; 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]; useEffect(() => {
model.position.lerp(targetPosition, 0.1); if (!groupRef.current) return;
model.rotation.x = THREE.MathUtils.lerp( const organization = email!.split("@")[1].split(".")[0];
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(() => { getActiveUsersData(organization).then((data) => {
if (!groupRef.current) return; const filteredData = data.cameraDatas.filter(
const organization = email!.split("@")[1].split(".")[0]; (camera: any) => camera.userData.email !== email
);
getActiveUsersData(organization).then((data) => { if (filteredData.length > 0) {
const filteredData = data.cameraDatas.filter( loader.load(camModel, (gltf) => {
(camera: any) => camera.userData.email !== email 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;
if (filteredData.length > 0) { return newModel;
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;
});
const users = filteredData.map((cam: any) => cam.userData); const users = filteredData.map((cam: any) => cam.userData);
setActiveUsers((prev: any) => dedupeUsers([...prev, ...users])); setActiveUsers((prev: any) => dedupeUsers([...prev, ...users]));
setCams((prev) => dedupeCams([...prev, ...newCams])); setCams((prev) => dedupeCams([...prev, ...newCams]));
}); });
} }
}); });
}, []); }, []);
return ( return (
<group <group ref={groupRef} name="Cam-Model-Group">
ref={groupRef} {cams.map((cam, index) => (
name="Cam-Model-Group" <primitive
visible={activeModule !== "visualization" ? true : false} key={cam.uuid}
> //eslint-disable-next-line
{cams.map((cam, index) => ( object={cam}
<primitive key={cam.uuid} object={cam}> visible={
<Html selectedUser?.name !== cam.userData.userName &&
as="div" activeModule !== "visualization"
center }
zIndexRange={[1, 0]} >
sprite <Html
style={{ as="div"
color: "white", center
textAlign: "center", zIndexRange={[1, 0]}
fontFamily: "Arial, sans-serif", sprite
display: `${activeModule !== "visualization" ? "" : "none"}`, style={{
}} color: "white",
position={[-0.015, 0, 0.7]} textAlign: "center",
> fontFamily: "Arial, sans-serif",
<CollabUserIcon display: `${activeModule !== "visualization" ? "" : "none"}`,
userImage={cam.userData.userImage ?? ""} }}
userName={cam.userData.userName} position={[-0.015, 0, 0.7]}
color={getAvatarColor(index, cam.userData.userName)} >
position={cam.position} <CollabUserIcon
rotation={cam.rotation} userImage={cam.userData.userImage ?? ""}
/> userName={cam.userData.userName}
</Html> color={getAvatarColor(index, cam.userData.userName)}
</primitive> position={cam.position}
))} rotation={cam.rotation}
</group> target={cam.userData.target}
); />
</Html>
</primitive>
))}
</group>
);
}; };
export default CamModelsGroup; export default CamModelsGroup;

View File

@@ -17,6 +17,11 @@ interface CollabUserIconProps {
y: number; y: number;
z: number; z: number;
}; };
target?: {
x: number;
y: number;
z: number;
};
} }
const CollabUserIcon: React.FC<CollabUserIconProps> = ({ const CollabUserIcon: React.FC<CollabUserIconProps> = ({
@@ -25,6 +30,7 @@ const CollabUserIcon: React.FC<CollabUserIconProps> = ({
color, color,
position, position,
rotation, rotation,
target,
}) => { }) => {
const { setSelectedUser } = useSelectedUserStore(); const { setSelectedUser } = useSelectedUserStore();
const { setCamMode } = useCamMode(); const { setCamMode } = useCamMode();
@@ -33,10 +39,13 @@ const CollabUserIcon: React.FC<CollabUserIconProps> = ({
<button <button
className="user-image-container" className="user-image-container"
onClick={() => { onClick={() => {
if(!position || !rotation) return; if (!position || !rotation || !target) return;
// Set the selected user in the store // Set the selected user in the store
setSelectedUser({ color: color, name: userName, location: { position, rotation } }); setSelectedUser({
color: color,
name: userName,
location: { position, rotation, target },
});
setCamMode("FollowPerson"); setCamMode("FollowPerson");
}} }}
> >

View File

@@ -11,18 +11,20 @@ const Collaboration: React.FC = () => {
const { camera, controls } = useThree(); // Access R3F camera and controls const { camera, controls } = useThree(); // Access R3F camera and controls
useEffect(() => { useEffect(() => {
if(camMode !== "FollowPerson") return; if (camMode !== "FollowPerson") return;
// If a user is selected, set the camera view to their location // If a user is selected, set the camera view to their location
// and update the camera and controls accordingly // and update the camera and controls accordingly
if (selectedUser?.location) { if (selectedUser?.location) {
const { position, rotation } = selectedUser.location; const { position, rotation, target } = selectedUser.location;
setCameraView({ if (rotation && target)
controls, setCameraView({
camera, controls,
position, camera,
rotation, position,
username: selectedUser.name, rotation,
}); target,
username: selectedUser.name,
});
} }
}, [selectedUser, camera, controls, camMode]); }, [selectedUser, camera, controls, camMode]);

View File

@@ -19,13 +19,16 @@ export default async function setCameraView({
}: SetCameraViewProps) { }: SetCameraViewProps) {
if (!controls || !camera) return; if (!controls || !camera) return;
if (target == null) return;
// Normalize position // Normalize position
const newPosition = position instanceof THREE.Vector3 const newPosition = position instanceof THREE.Vector3
? position ? position
: new THREE.Vector3(position.x, position.y, position.z); : new THREE.Vector3(position.x, position.y, position.z);
if (controls.setTarget) { const newTarget = target instanceof THREE.Vector3 ? target : new THREE.Vector3(target.x, target.y, target.z);
controls?.setLookAt(...newPosition.toArray(), newPosition.x, 0, newPosition.z, true);
}
if (controls.setTarget) {
controls?.setLookAt(...newPosition.toArray(), ...newTarget.toArray(), true);
}
} }

View File

@@ -9,7 +9,12 @@ interface SelectedUser {
y: number; y: number;
z: number; z: number;
}; };
rotation: { rotation?: {
x: number;
y: number;
z: number;
};
target?: {
x: number; x: number;
y: number; y: number;
z: number; z: number;

View File

@@ -15,4 +15,7 @@ export type ActiveUser = {
userName: string; userName: string;
email: string; email: string;
activeStatus?: string; // Optional property activeStatus?: string; // Optional property
position?: { x: number; y: number; z: number; };
rotation?: { x: number; y: number; z: number; };
target?: { x: number; y: number; z: number; };
}; };