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:
@@ -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 = () => {
|
||||
<div className="other-guest">+{guestUsers.length - 3}</div>
|
||||
)}
|
||||
{guestUsers.slice(0, 3).map((user, index) => (
|
||||
<div
|
||||
key={index}
|
||||
<button
|
||||
key={`${index}-${user.userName}`}
|
||||
className="user-profile"
|
||||
style={{ background: getAvatarColor(index, user.userName) }}
|
||||
onClick={() => {
|
||||
handleUserFollow(user, index);
|
||||
}}
|
||||
>
|
||||
{user.userName[0]}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="user-profile-container">
|
||||
|
||||
@@ -13,13 +13,13 @@ const FollowPerson: React.FC = () => {
|
||||
// eslint-disable-next-line
|
||||
<div
|
||||
className="follow-person-container"
|
||||
onClick={() => {
|
||||
onPointerDown={() => {
|
||||
clearSelectedUser();
|
||||
setCamMode("FirstPerson");
|
||||
}}
|
||||
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>
|
||||
)}
|
||||
</RenderOverlay>
|
||||
|
||||
@@ -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<THREE.Group>(null);
|
||||
const email = localStorage.getItem("email");
|
||||
const { setActiveUsers } = useActiveUsers();
|
||||
const { socket } = useSocketStore();
|
||||
const { activeModule } = useModuleStore();
|
||||
const navigate = useNavigate();
|
||||
const groupRef = useRef<THREE.Group>(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<any[]>([]);
|
||||
const [models, setModels] = useState<Record<string, { targetPosition: THREE.Vector3; targetRotation: THREE.Euler, target: THREE.Vector3 }>>({});
|
||||
const [cams, setCams] = useState<any[]>([]);
|
||||
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 (
|
||||
<group
|
||||
ref={groupRef}
|
||||
name="Cam-Model-Group"
|
||||
visible={activeModule !== "visualization" ? true : false}
|
||||
>
|
||||
{cams.map((cam, index) => (
|
||||
<primitive key={cam.uuid} object={cam}>
|
||||
<Html
|
||||
as="div"
|
||||
center
|
||||
zIndexRange={[1, 0]}
|
||||
sprite
|
||||
style={{
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
display: `${activeModule !== "visualization" ? "" : "none"}`,
|
||||
}}
|
||||
position={[-0.015, 0, 0.7]}
|
||||
>
|
||||
<CollabUserIcon
|
||||
userImage={cam.userData.userImage ?? ""}
|
||||
userName={cam.userData.userName}
|
||||
color={getAvatarColor(index, cam.userData.userName)}
|
||||
position={cam.position}
|
||||
rotation={cam.rotation}
|
||||
/>
|
||||
</Html>
|
||||
</primitive>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
const users = filteredData.map((cam: any) => cam.userData);
|
||||
setActiveUsers((prev: any) => dedupeUsers([...prev, ...users]));
|
||||
setCams((prev) => dedupeCams([...prev, ...newCams]));
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<group ref={groupRef} name="Cam-Model-Group">
|
||||
{cams.map((cam, index) => (
|
||||
<primitive
|
||||
key={cam.uuid}
|
||||
//eslint-disable-next-line
|
||||
object={cam}
|
||||
visible={
|
||||
selectedUser?.name !== cam.userData.userName &&
|
||||
activeModule !== "visualization"
|
||||
}
|
||||
>
|
||||
<Html
|
||||
as="div"
|
||||
center
|
||||
zIndexRange={[1, 0]}
|
||||
sprite
|
||||
style={{
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
display: `${activeModule !== "visualization" ? "" : "none"}`,
|
||||
}}
|
||||
position={[-0.015, 0, 0.7]}
|
||||
>
|
||||
<CollabUserIcon
|
||||
userImage={cam.userData.userImage ?? ""}
|
||||
userName={cam.userData.userName}
|
||||
color={getAvatarColor(index, cam.userData.userName)}
|
||||
position={cam.position}
|
||||
rotation={cam.rotation}
|
||||
target={cam.userData.target}
|
||||
/>
|
||||
</Html>
|
||||
</primitive>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CamModelsGroup;
|
||||
|
||||
@@ -17,6 +17,11 @@ interface CollabUserIconProps {
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
target?: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
const CollabUserIcon: React.FC<CollabUserIconProps> = ({
|
||||
@@ -25,6 +30,7 @@ const CollabUserIcon: React.FC<CollabUserIconProps> = ({
|
||||
color,
|
||||
position,
|
||||
rotation,
|
||||
target,
|
||||
}) => {
|
||||
const { setSelectedUser } = useSelectedUserStore();
|
||||
const { setCamMode } = useCamMode();
|
||||
@@ -33,10 +39,13 @@ const CollabUserIcon: React.FC<CollabUserIconProps> = ({
|
||||
<button
|
||||
className="user-image-container"
|
||||
onClick={() => {
|
||||
if(!position || !rotation) return;
|
||||
|
||||
if (!position || !rotation || !target) return;
|
||||
// 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");
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -11,18 +11,20 @@ const Collaboration: React.FC = () => {
|
||||
const { camera, controls } = useThree(); // Access R3F camera and controls
|
||||
|
||||
useEffect(() => {
|
||||
if(camMode !== "FollowPerson") return;
|
||||
if (camMode !== "FollowPerson") return;
|
||||
// If a user is selected, set the camera view to their location
|
||||
// and update the camera and controls accordingly
|
||||
if (selectedUser?.location) {
|
||||
const { position, rotation } = selectedUser.location;
|
||||
setCameraView({
|
||||
controls,
|
||||
camera,
|
||||
position,
|
||||
rotation,
|
||||
username: selectedUser.name,
|
||||
});
|
||||
const { position, rotation, target } = selectedUser.location;
|
||||
if (rotation && target)
|
||||
setCameraView({
|
||||
controls,
|
||||
camera,
|
||||
position,
|
||||
rotation,
|
||||
target,
|
||||
username: selectedUser.name,
|
||||
});
|
||||
}
|
||||
}, [selectedUser, camera, controls, camMode]);
|
||||
|
||||
|
||||
@@ -19,13 +19,16 @@ export default async function setCameraView({
|
||||
}: SetCameraViewProps) {
|
||||
if (!controls || !camera) return;
|
||||
|
||||
if (target == null) return;
|
||||
|
||||
// Normalize position
|
||||
const newPosition = position instanceof THREE.Vector3
|
||||
? position
|
||||
: new THREE.Vector3(position.x, position.y, position.z);
|
||||
|
||||
if (controls.setTarget) {
|
||||
controls?.setLookAt(...newPosition.toArray(), newPosition.x, 0, newPosition.z, true);
|
||||
}
|
||||
const newTarget = target instanceof THREE.Vector3 ? target : new THREE.Vector3(target.x, target.y, target.z);
|
||||
|
||||
if (controls.setTarget) {
|
||||
controls?.setLookAt(...newPosition.toArray(), ...newTarget.toArray(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ interface SelectedUser {
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
rotation: {
|
||||
rotation?: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
target?: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
|
||||
3
app/src/types/users.d.ts
vendored
3
app/src/types/users.d.ts
vendored
@@ -15,4 +15,7 @@ export type ActiveUser = {
|
||||
userName: string;
|
||||
email: string;
|
||||
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; };
|
||||
};
|
||||
Reference in New Issue
Block a user