first commit
This commit is contained in:
311
app/src/modules/collaboration/camera/collabCams.tsx
Normal file
311
app/src/modules/collaboration/camera/collabCams.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
import * as THREE from "three";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import camModel from "../../../assets/gltf-glb/camera face 2.gltf";
|
||||
import getActiveUsersData from "../../../services/factoryBuilder/collab/getActiveUsers";
|
||||
import { useActiveUsers, useCamMode, useSocketStore } from "../../../store/builder/store";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Html } from "@react-three/drei";
|
||||
import CollabUserIcon from "./collabUserIcon";
|
||||
import useModuleStore from "../../../store/useModuleStore";
|
||||
import { getAvatarColor } from "../functions/getAvatarColor";
|
||||
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
|
||||
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||
import setCameraView from "../functions/setCameraView";
|
||||
|
||||
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 { selectedUser, setSelectedUser } = useSelectedUserStore();
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
|
||||
// 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 { camMode } = useCamMode();
|
||||
const { camera, controls } = useThree(); // Access R3F camera and controls
|
||||
|
||||
useEffect(() => {
|
||||
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, target } = selectedUser.location;
|
||||
if (rotation && target)
|
||||
setCameraView({
|
||||
controls,
|
||||
camera,
|
||||
position,
|
||||
rotation,
|
||||
target,
|
||||
username: selectedUser.name,
|
||||
});
|
||||
}
|
||||
}, [selectedUser, camera, controls, camMode]);
|
||||
|
||||
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 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("/");
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
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])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
socket.on("v1:camera:Response:update", (data: any) => {
|
||||
// console.log('data: ', data);
|
||||
if (
|
||||
!groupRef.current ||
|
||||
socket.id === data.socketId ||
|
||||
organization !== data.organization
|
||||
)
|
||||
return;
|
||||
|
||||
if (selectedUser && selectedUser?.id === data.data.userId) {
|
||||
setSelectedUser({
|
||||
color: selectedUser.color,
|
||||
name: selectedUser.name,
|
||||
id: selectedUser.id,
|
||||
location: {
|
||||
position: data.data.position,
|
||||
rotation: data.data.rotation,
|
||||
target: data.data.target,
|
||||
},
|
||||
});
|
||||
}
|
||||
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("v1:camera:Response:update");
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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;
|
||||
|
||||
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];
|
||||
|
||||
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;
|
||||
cam.userData.position = newModel.position;
|
||||
cam.userData.rotation = newModel.rotation;
|
||||
newModel.userData.target = cam.target;
|
||||
|
||||
return newModel;
|
||||
});
|
||||
|
||||
const users = filteredData.map((cam: any) => cam.userData);
|
||||
setActiveUsers((prev: any) => dedupeUsers([...prev, ...users]));
|
||||
setCams((prev) => dedupeCams([...prev, ...newCams]));
|
||||
});
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
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" &&
|
||||
!isPlaying
|
||||
}
|
||||
>
|
||||
<Html
|
||||
as="div"
|
||||
center
|
||||
zIndexRange={[1, 0]}
|
||||
sprite
|
||||
style={{
|
||||
color: "white",
|
||||
textAlign: "center",
|
||||
fontFamily: "Arial, sans-serif",
|
||||
display: `${activeModule !== "visualization" ? "" : "none"}`,
|
||||
opacity: `${selectedUser?.name !== cam.userData.userName && !isPlaying
|
||||
? 1
|
||||
: 0
|
||||
}`,
|
||||
transition: "opacity .2s ease",
|
||||
}}
|
||||
position={[-0.015, 0, 0.7]}
|
||||
>
|
||||
<CollabUserIcon
|
||||
userImage={cam.userData.userImage ?? ""}
|
||||
userName={cam.userData.userName}
|
||||
id={cam.userData._id}
|
||||
color={getAvatarColor(index, cam.userData.userName)}
|
||||
position={cam.position}
|
||||
rotation={cam.rotation}
|
||||
target={cam.userData.target}
|
||||
/>
|
||||
</Html>
|
||||
</primitive>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
|
||||
export default CamModelsGroup;
|
||||
69
app/src/modules/collaboration/camera/collabUserIcon.tsx
Normal file
69
app/src/modules/collaboration/camera/collabUserIcon.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from "react";
|
||||
import CustomAvatar from "../users/Avatar";
|
||||
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
|
||||
import { useCamMode } from "../../../store/builder/store";
|
||||
|
||||
interface CollabUserIconProps {
|
||||
userName: string;
|
||||
userImage?: string;
|
||||
color: string;
|
||||
id: string;
|
||||
position?: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
rotation?: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
target?: {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
};
|
||||
}
|
||||
|
||||
const CollabUserIcon: React.FC<CollabUserIconProps> = ({
|
||||
userImage,
|
||||
userName,
|
||||
id,
|
||||
color,
|
||||
position,
|
||||
rotation,
|
||||
target,
|
||||
}) => {
|
||||
const { setSelectedUser } = useSelectedUserStore();
|
||||
const { setCamMode } = useCamMode();
|
||||
return (
|
||||
<div className="collab-user-live-container">
|
||||
<button
|
||||
id="live-user-button"
|
||||
className="user-image-container"
|
||||
onClick={() => {
|
||||
if (!position || !rotation || !target) return;
|
||||
// Set the selected user in the store
|
||||
setSelectedUser({
|
||||
id: id,
|
||||
color: color,
|
||||
name: userName,
|
||||
location: { position, rotation, target },
|
||||
});
|
||||
setCamMode("FollowPerson");
|
||||
}}
|
||||
>
|
||||
{userImage ? (
|
||||
<img className="user-image" src={userImage} alt={userName} />
|
||||
) : (
|
||||
<CustomAvatar name={userName} color={color} />
|
||||
)}
|
||||
</button>
|
||||
<div className="user-name" style={{ backgroundColor: color }}>
|
||||
{userName}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollabUserIcon;
|
||||
18
app/src/modules/collaboration/collaboration.tsx
Normal file
18
app/src/modules/collaboration/collaboration.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import CamModelsGroup from "./camera/collabCams";
|
||||
import CommentsGroup from "./comments/commentsGroup";
|
||||
|
||||
const Collaboration: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<CamModelsGroup />
|
||||
|
||||
<CommentsGroup />
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Collaboration;
|
||||
134
app/src/modules/collaboration/comments/commentsGroup.tsx
Normal file
134
app/src/modules/collaboration/comments/commentsGroup.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useActiveTool } from "../../../store/builder/store"
|
||||
import { useThree } from "@react-three/fiber";
|
||||
import { MathUtils, Vector3 } from "three";
|
||||
import { useCommentStore } from "../../../store/collaboration/useCommentStore";
|
||||
import CommentInstances from "./instances/commentInstances";
|
||||
import { Sphere } from "@react-three/drei";
|
||||
|
||||
function CommentsGroup() {
|
||||
const { gl, raycaster, camera, scene, pointer } = useThree();
|
||||
const { activeTool } = useActiveTool();
|
||||
const { addComment } = useCommentStore();
|
||||
const [hoverPos, setHoverPos] = useState<Vector3 | null>(null);
|
||||
|
||||
const userId = localStorage.getItem('userId') ?? '';
|
||||
|
||||
useEffect(() => {
|
||||
const canvasElement = gl.domElement;
|
||||
|
||||
let drag = false;
|
||||
let isLeftMouseDown = false;
|
||||
|
||||
const onMouseDown = (evt: MouseEvent) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = true;
|
||||
drag = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseUp = (evt: MouseEvent) => {
|
||||
if (evt.button === 0) {
|
||||
isLeftMouseDown = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onMouseMove = () => {
|
||||
if (isLeftMouseDown) {
|
||||
drag = true;
|
||||
}
|
||||
|
||||
raycaster.setFromCamera(pointer, camera);
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const point = intersects[0].point;
|
||||
setHoverPos(new Vector3(point.x, Math.max(point.y, 0), point.z));
|
||||
} else {
|
||||
setHoverPos(null);
|
||||
}
|
||||
};
|
||||
|
||||
const onMouseClick = () => {
|
||||
if (drag) return;
|
||||
|
||||
const intersects = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.filter(
|
||||
(intersect) =>
|
||||
!intersect.object.name.includes("Roof") &&
|
||||
!intersect.object.name.includes("MeasurementReference") &&
|
||||
!intersect.object.name.includes("agv-collider") &&
|
||||
!intersect.object.name.includes("zonePlane") &&
|
||||
!intersect.object.name.includes("SelectionGroup") &&
|
||||
!intersect.object.name.includes("selectionAssetGroup") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBoxLine") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingBox") &&
|
||||
!intersect.object.name.includes("SelectionGroupBoundingLine") &&
|
||||
intersect.object.type !== "GridHelper"
|
||||
);
|
||||
if (intersects.length > 0) {
|
||||
const position = new Vector3(intersects[0].point.x, Math.max(intersects[0].point.y, 0), intersects[0].point.z);
|
||||
|
||||
const comment: CommentSchema = {
|
||||
state: 'active',
|
||||
commentId: MathUtils.generateUUID(),
|
||||
creatorId: userId,
|
||||
createdAt: new Date().toISOString(),
|
||||
comment: '',
|
||||
lastUpdatedAt: new Date().toISOString(),
|
||||
position: position.toArray(),
|
||||
rotation: [0, 0, 0],
|
||||
replies: []
|
||||
}
|
||||
|
||||
addComment(comment);
|
||||
setHoverPos(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (activeTool === 'comment') {
|
||||
canvasElement.addEventListener("mousedown", onMouseDown);
|
||||
canvasElement.addEventListener("mouseup", onMouseUp);
|
||||
canvasElement.addEventListener("mousemove", onMouseMove);
|
||||
canvasElement.addEventListener("click", onMouseClick);
|
||||
} else {
|
||||
setHoverPos(null);
|
||||
}
|
||||
|
||||
return () => {
|
||||
canvasElement.removeEventListener("mousedown", onMouseDown);
|
||||
canvasElement.removeEventListener("mouseup", onMouseUp);
|
||||
canvasElement.removeEventListener("mousemove", onMouseMove);
|
||||
canvasElement.removeEventListener("click", onMouseClick);
|
||||
};
|
||||
}, [activeTool, camera])
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommentInstances />
|
||||
|
||||
{hoverPos && (
|
||||
<Sphere name={'commentHolder'} args={[0.1, 16, 16]} position={hoverPos}>
|
||||
<meshStandardMaterial color="orange" />
|
||||
</Sphere>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommentsGroup
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Html, TransformControls } from '@react-three/drei';
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
|
||||
import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
|
||||
import CommentThreads from '../../../../../components/ui/collaboration/CommentThreads';
|
||||
|
||||
function CommentInstance({ comment }: { comment: CommentSchema }) {
|
||||
const { isPlaying } = usePlayButtonStore();
|
||||
const CommentRef = useRef(null);
|
||||
const [selectedComment, setSelectedComment] = useState<CommentSchema | null>(null);
|
||||
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const keyCombination = detectModifierKeys(e);
|
||||
if (!selectedComment) return;
|
||||
if (keyCombination === "G") {
|
||||
setTransformMode((prev) => (prev === "translate" ? null : "translate"));
|
||||
}
|
||||
if (keyCombination === "R") {
|
||||
setTransformMode((prev) => (prev === "rotate" ? null : "rotate"));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [selectedComment]);
|
||||
|
||||
const commentClicked = () => {
|
||||
console.log('hii');
|
||||
setSelectedComment(comment);
|
||||
}
|
||||
|
||||
if (comment.state === 'inactive' || isPlaying) return null;
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
<Html
|
||||
ref={CommentRef}
|
||||
zIndexRange={[1, 0]}
|
||||
prepend
|
||||
sprite
|
||||
center
|
||||
position={comment.position}
|
||||
rotation={comment.rotation}
|
||||
className='comments-main-wrapper'
|
||||
>
|
||||
<CommentThreads commentClicked={commentClicked} />
|
||||
</Html>
|
||||
{CommentRef.current && transformMode && (
|
||||
<TransformControls
|
||||
object={CommentRef.current}
|
||||
mode={transformMode}
|
||||
onMouseUp={(e) => {
|
||||
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommentInstance;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import CommentInstance from './commentInstance/commentInstance'
|
||||
import { useCommentStore } from '../../../../store/collaboration/useCommentStore'
|
||||
|
||||
function CommentInstances() {
|
||||
const { comments } = useCommentStore();
|
||||
|
||||
useEffect(() => {
|
||||
// console.log('comments: ', comments);
|
||||
}, [comments])
|
||||
|
||||
return (
|
||||
<>
|
||||
{comments.map((comment: CommentSchema) => (
|
||||
<React.Fragment key={comment.commentId}>
|
||||
<CommentInstance comment={comment} />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CommentInstances
|
||||
56
app/src/modules/collaboration/functions/getAvatarColor.ts
Normal file
56
app/src/modules/collaboration/functions/getAvatarColor.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
const avatarColors: string[] = [
|
||||
"#FF5733", // Vivid Orange
|
||||
"#48ac2a", // Leaf Green
|
||||
"#0050eb", // Bright Blue
|
||||
"#FF33A1", // Hot Pink
|
||||
"#FF8C33", // Sunset Orange
|
||||
"#8C33FF", // Violet Purple
|
||||
"#FF3333", // Fiery Red
|
||||
"#43c06d", // Emerald Green
|
||||
"#A133FF", // Royal Purple
|
||||
"#C70039", // Crimson Red
|
||||
"#900C3F", // Deep Burgundy
|
||||
"#581845", // Plum Purple
|
||||
"#3859AD", // Steel Blue
|
||||
"#08873E", // Forest Green
|
||||
"#E74C3C", // Cherry Red
|
||||
"#00adff", // Sky Blue
|
||||
"#DBAD05", // Golden Yellow
|
||||
"#A13E31", // Brick Red
|
||||
"#94C40E", // Lime Green
|
||||
"#060C47", // Midnight Blue
|
||||
"#2FAFAF", // Teal
|
||||
];
|
||||
|
||||
export function getAvatarColor(index: number, name?: string): string {
|
||||
// Check if the color is already stored in localStorage
|
||||
const localStorageKey = "userAvatarColors";
|
||||
// Check if local storage is available
|
||||
if (name) {
|
||||
let userColors = JSON.parse(localStorage.getItem(localStorageKey) ?? "{}");
|
||||
|
||||
// Check if the user already has an assigned color
|
||||
if (userColors[name]) {
|
||||
return userColors[name];
|
||||
}
|
||||
|
||||
// Find a new color not already assigned
|
||||
const usedColors = Object.values(userColors);
|
||||
const availableColors = avatarColors.filter(color => !usedColors.includes(color));
|
||||
|
||||
// Assign a new color
|
||||
const assignedColor = availableColors.length > 0
|
||||
? availableColors[0]
|
||||
: avatarColors[index % avatarColors.length];
|
||||
|
||||
userColors[name] = assignedColor;
|
||||
|
||||
// Save back to local storage
|
||||
localStorage.setItem(localStorageKey, JSON.stringify(userColors));
|
||||
|
||||
return assignedColor;
|
||||
}
|
||||
|
||||
// Fallback: Assign a color using the index if no name or local storage is unavailable
|
||||
return avatarColors[index % avatarColors.length];
|
||||
}
|
||||
10
app/src/modules/collaboration/functions/getInitials.ts
Normal file
10
app/src/modules/collaboration/functions/getInitials.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const getInitials = (fullName: string): string => {
|
||||
// Extract initials from the name
|
||||
const words = fullName.split(" ");
|
||||
const initials = words
|
||||
.map((word) => word[0])
|
||||
.slice(0, 2)
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
return initials;
|
||||
};
|
||||
34
app/src/modules/collaboration/functions/setCameraView.ts
Normal file
34
app/src/modules/collaboration/functions/setCameraView.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
interface SetCameraViewProps {
|
||||
controls: any;
|
||||
camera: THREE.Camera;
|
||||
position: THREE.Vector3 | { x: number; y: number; z: number };
|
||||
rotation: THREE.Euler | { x: number; y: number; z: number };
|
||||
username?: string;
|
||||
target?: THREE.Vector3 | { x: number; y: number; z: number };
|
||||
}
|
||||
|
||||
export default async function setCameraView({
|
||||
controls,
|
||||
camera,
|
||||
position,
|
||||
rotation,
|
||||
username,
|
||||
target
|
||||
}: 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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
884
app/src/modules/collaboration/socket/socketResponses.dev.tsx
Normal file
884
app/src/modules/collaboration/socket/socketResponses.dev.tsx
Normal file
@@ -0,0 +1,884 @@
|
||||
import { useEffect } from "react";
|
||||
import * as THREE from "three";
|
||||
import gsap from "gsap";
|
||||
import { GLTF, GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||
import {
|
||||
useSocketStore,
|
||||
useActiveLayer,
|
||||
useWallItems,
|
||||
useLayers,
|
||||
useUpdateScene,
|
||||
useWalls,
|
||||
useDeletedLines,
|
||||
useNewLines,
|
||||
useZonePoints,
|
||||
useZones,
|
||||
} from "../../../store/builder/store";
|
||||
|
||||
import * as Types from "../../../types/world/worldTypes";
|
||||
import * as CONSTANTS from "../../../types/world/worldConstants";
|
||||
|
||||
// import { setFloorItemApi } from "../../services/factoryBuilder/assest/floorAsset/setFloorItemApi";
|
||||
import objectLineToArray from "../../builder/geomentries/lines/lineConvertions/objectLineToArray";
|
||||
import addLineToScene from "../../builder/geomentries/lines/addLineToScene";
|
||||
import updateLinesPositions from "../../builder/geomentries/lines/updateLinesPositions";
|
||||
import updateLines from "../../builder/geomentries/lines/updateLines";
|
||||
import updateDistanceText from "../../builder/geomentries/lines/updateDistanceText";
|
||||
import updateFloorLines from "../../builder/geomentries/floors/updateFloorLines";
|
||||
import loadWalls from "../../builder/geomentries/walls/loadWalls";
|
||||
import RemoveConnectedLines from "../../builder/geomentries/lines/removeConnectedLines";
|
||||
import Layer2DVisibility from "../../builder/geomentries/layers/layer2DVisibility";
|
||||
import { retrieveGLTF, storeGLTF } from "../../../utils/indexDB/idbUtils";
|
||||
import { getZonesApi } from "../../../services/factoryBuilder/zones/getZonesApi";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useAssetsStore } from "../../../store/builder/useAssetStore";
|
||||
import { useEventsStore } from "../../../store/simulation/useEventsStore";
|
||||
import { useProductStore } from "../../../store/simulation/useProductStore";
|
||||
|
||||
export default function SocketResponses({
|
||||
floorPlanGroup,
|
||||
lines,
|
||||
floorGroup,
|
||||
floorGroupAisle,
|
||||
scene,
|
||||
onlyFloorlines,
|
||||
itemsGroup,
|
||||
isTempLoader,
|
||||
tempLoader,
|
||||
currentLayerPoint,
|
||||
floorPlanGroupPoint,
|
||||
floorPlanGroupLine,
|
||||
zoneGroup,
|
||||
dragPointControls,
|
||||
}: any) {
|
||||
const { socket } = useSocketStore();
|
||||
const { activeLayer, setActiveLayer } = useActiveLayer();
|
||||
const { wallItems, setWallItems } = useWallItems();
|
||||
const { setLayers } = useLayers();
|
||||
const { setUpdateScene } = useUpdateScene();
|
||||
const { setWalls } = useWalls();
|
||||
const { setDeletedLines } = useDeletedLines();
|
||||
const { setNewLines } = useNewLines();
|
||||
const { zones, setZones } = useZones();
|
||||
const { zonePoints, setZonePoints } = useZonePoints();
|
||||
const { projectId } = useParams();
|
||||
const { addAsset, updateAsset, removeAsset } = useAssetsStore();
|
||||
|
||||
useEffect(() => {
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
if (!socket) return;
|
||||
|
||||
socket.on("cameraCreateResponse", (data: any) => {
|
||||
//
|
||||
});
|
||||
|
||||
socket.on("userConnectRespones", (data: any) => {
|
||||
//
|
||||
});
|
||||
|
||||
socket.on("userDisConnectRespones", (data: any) => {
|
||||
//
|
||||
});
|
||||
|
||||
socket.on("v1:camera:Response:update", (data: any) => {
|
||||
//
|
||||
});
|
||||
|
||||
socket.on("EnvironmentUpdateResponse", (data: any) => {
|
||||
//
|
||||
});
|
||||
|
||||
socket.on("v1:model-asset:response:add", async (data: any) => {
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "Model created successfully") {
|
||||
try {
|
||||
|
||||
const asset: Asset = {
|
||||
modelUuid: data.data.modelUuid,
|
||||
modelName: data.data.modelName,
|
||||
assetId: data.data.modelfileID,
|
||||
position: data.data.position,
|
||||
rotation: [data.data.rotation.x, data.data.rotation.y, data.data.rotation.z],
|
||||
isLocked: data.data.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: data.data.isVisible,
|
||||
opacity: 1,
|
||||
}
|
||||
|
||||
addAsset(asset);
|
||||
|
||||
echo.success("Added model through collaboration");
|
||||
} catch (error) {
|
||||
echo.error("Failed to create model through collaboration");
|
||||
}
|
||||
} else if (data.message === "Model updated successfully") {
|
||||
try {
|
||||
|
||||
const asset: Asset = {
|
||||
modelUuid: data.data.modelUuid,
|
||||
modelName: data.data.modelName,
|
||||
assetId: data.data.modelfileID,
|
||||
position: data.data.position,
|
||||
rotation: [data.data.rotation.x, data.data.rotation.y, data.data.rotation.z],
|
||||
isLocked: data.data.isLocked,
|
||||
isCollidable: false,
|
||||
isVisible: data.data.isVisible,
|
||||
opacity: 1,
|
||||
}
|
||||
|
||||
updateAsset(asset.modelUuid, {
|
||||
position: asset.position,
|
||||
rotation: asset.rotation,
|
||||
});
|
||||
|
||||
echo.success("Updated model through collaboration");
|
||||
} catch (error) {
|
||||
echo.error("Failed to update model through collaboration");
|
||||
}
|
||||
} else {
|
||||
echo.error("Failed executing action from collaboration");
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("v1:model-asset:response:delete", (data: any) => {
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "Model deleted successfully") {
|
||||
try {
|
||||
const deletedUUID = data.data.modelUuid;
|
||||
|
||||
useEventsStore.getState().removeEvent(deletedUUID);
|
||||
useProductStore.getState().deleteEvent(deletedUUID);
|
||||
|
||||
removeAsset(deletedUUID);
|
||||
|
||||
echo.success("Model Removed successfully through collaboration");
|
||||
} catch (error) {
|
||||
echo.error("Failed to remove model through collaboration");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("v1:Line:response:update", (data: any) => {
|
||||
// console.log('data: ', data);
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "line updated") {
|
||||
const DraggedUUID = data.data.uuid;
|
||||
const DraggedPosition = new THREE.Vector3(
|
||||
data.data.position.x,
|
||||
data.data.position.y,
|
||||
data.data.position.z
|
||||
);
|
||||
|
||||
const point = floorPlanGroupPoint.current.getObjectByProperty(
|
||||
"uuid",
|
||||
DraggedUUID
|
||||
);
|
||||
point.position.set(
|
||||
DraggedPosition.x,
|
||||
DraggedPosition.y,
|
||||
DraggedPosition.z
|
||||
);
|
||||
const affectedLines = updateLinesPositions(
|
||||
{ uuid: DraggedUUID, position: DraggedPosition },
|
||||
lines
|
||||
);
|
||||
|
||||
updateLines(floorPlanGroupLine, affectedLines);
|
||||
updateDistanceText(scene, floorPlanGroupLine, affectedLines);
|
||||
updateFloorLines(onlyFloorlines, {
|
||||
uuid: DraggedUUID,
|
||||
position: DraggedPosition,
|
||||
});
|
||||
|
||||
loadWalls(lines, setWalls);
|
||||
setUpdateScene(true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("v1:Line:response:delete", (data: any) => {
|
||||
// console.log('data: ', data);
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "line deleted") {
|
||||
const line = objectLineToArray(data.data);
|
||||
const linePoints = line;
|
||||
const connectedpoints = [linePoints[0][1], linePoints[1][1]];
|
||||
|
||||
onlyFloorlines.current = onlyFloorlines.current
|
||||
.map((floorline: any) =>
|
||||
floorline.filter(
|
||||
(line: any) =>
|
||||
line[0][1] !== connectedpoints[0] &&
|
||||
line[1][1] !== connectedpoints[1]
|
||||
)
|
||||
)
|
||||
.filter((floorline: any) => floorline.length > 0);
|
||||
|
||||
const removedLine = lines.current.find(
|
||||
(item: any) =>
|
||||
(item[0][1] === linePoints[0][1] &&
|
||||
item[1][1] === linePoints[1][1]) ||
|
||||
(item[0][1] === linePoints[1][1] && item[1][1] === linePoints[0][1])
|
||||
);
|
||||
lines.current = lines.current.filter(
|
||||
(item: any) => item !== removedLine
|
||||
);
|
||||
|
||||
floorPlanGroupLine.current.children.forEach((line: any) => {
|
||||
const linePoints = line.userData.linePoints as [
|
||||
number,
|
||||
string,
|
||||
number
|
||||
][];
|
||||
const uuid1 = linePoints[0][1];
|
||||
const uuid2 = linePoints[1][1];
|
||||
|
||||
if (
|
||||
(uuid1 === connectedpoints[0] && uuid2 === connectedpoints[1]) ||
|
||||
(uuid1 === connectedpoints[1] && uuid2 === connectedpoints[0])
|
||||
) {
|
||||
line.material.dispose();
|
||||
line.geometry.dispose();
|
||||
floorPlanGroupLine.current.remove(line);
|
||||
setDeletedLines([line.userData.linePoints]);
|
||||
}
|
||||
});
|
||||
|
||||
connectedpoints.forEach((pointUUID) => {
|
||||
let isConnected = false;
|
||||
floorPlanGroupLine.current.children.forEach((line: any) => {
|
||||
const linePoints = line.userData.linePoints;
|
||||
const uuid1 = linePoints[0][1];
|
||||
const uuid2 = linePoints[1][1];
|
||||
if (uuid1 === pointUUID || uuid2 === pointUUID) {
|
||||
isConnected = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!isConnected) {
|
||||
floorPlanGroupPoint.current.children.forEach((point: any) => {
|
||||
if (point.uuid === pointUUID) {
|
||||
point.material.dispose();
|
||||
point.geometry.dispose();
|
||||
floorPlanGroupPoint.current.remove(point);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
loadWalls(lines, setWalls);
|
||||
setUpdateScene(true);
|
||||
|
||||
echo.success("Line Removed!");
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("v1:Line:response:delete:point", (data: any) => {
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "point deleted") {
|
||||
const point = floorPlanGroupPoint.current?.getObjectByProperty(
|
||||
"uuid",
|
||||
data.data
|
||||
);
|
||||
point.material.dispose();
|
||||
point.geometry.dispose();
|
||||
floorPlanGroupPoint.current.remove(point);
|
||||
|
||||
onlyFloorlines.current = onlyFloorlines.current
|
||||
.map((floorline: any) =>
|
||||
floorline.filter(
|
||||
(line: any) =>
|
||||
line[0][1] !== data.data && line[1][1] !== data.data
|
||||
)
|
||||
)
|
||||
.filter((floorline: any) => floorline.length > 0);
|
||||
|
||||
RemoveConnectedLines(
|
||||
data.data,
|
||||
floorPlanGroupLine,
|
||||
floorPlanGroupPoint,
|
||||
setDeletedLines,
|
||||
lines
|
||||
);
|
||||
|
||||
loadWalls(lines, setWalls);
|
||||
setUpdateScene(true);
|
||||
|
||||
echo.success("Point Removed!");
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("v1:Line:response:delete:layer", async (data: any) => {
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "layer deleted") {
|
||||
setActiveLayer(1);
|
||||
const removedLayer = data.data;
|
||||
const removedLines: Types.Lines = lines.current.filter(
|
||||
(line: any) => line[0][2] === removedLayer
|
||||
);
|
||||
|
||||
////////// Remove Points and lines from the removed layer //////////
|
||||
|
||||
removedLines.forEach(async (line) => {
|
||||
line.forEach(async (removedPoint) => {
|
||||
const removableLines: THREE.Mesh[] = [];
|
||||
const connectedpoints: string[] = [];
|
||||
|
||||
floorPlanGroupLine.current.children.forEach((line: any) => {
|
||||
const linePoints = line.userData.linePoints as [
|
||||
number,
|
||||
string,
|
||||
number
|
||||
][];
|
||||
const uuid1 = linePoints[0][1];
|
||||
const uuid2 = linePoints[1][1];
|
||||
|
||||
if (uuid1 === removedPoint[1] || uuid2 === removedPoint[1]) {
|
||||
connectedpoints.push(uuid1 === removedPoint[1] ? uuid2 : uuid1);
|
||||
removableLines.push(line as THREE.Mesh);
|
||||
}
|
||||
});
|
||||
|
||||
if (removableLines.length > 0) {
|
||||
removableLines.forEach((line: any) => {
|
||||
lines.current = lines.current.filter(
|
||||
(item: any) =>
|
||||
JSON.stringify(item) !==
|
||||
JSON.stringify(line.userData.linePoints)
|
||||
);
|
||||
line.material.dispose();
|
||||
line.geometry.dispose();
|
||||
floorPlanGroupLine.current.remove(line);
|
||||
});
|
||||
}
|
||||
|
||||
const point = floorPlanGroupPoint.current.getObjectByProperty(
|
||||
"uuid",
|
||||
removedPoint[1]
|
||||
);
|
||||
if (point) {
|
||||
point.material.dispose();
|
||||
point.geometry.dispose();
|
||||
floorPlanGroupPoint.current.remove(point);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
////////// Update the remaining lines layer values in the userData and in lines.current //////////
|
||||
|
||||
let remaining = lines.current.filter(
|
||||
(line: any) => line[0][2] !== removedLayer
|
||||
);
|
||||
let updatedLines: Types.Lines = [];
|
||||
remaining.forEach((line: any) => {
|
||||
let newLines = JSON.parse(JSON.stringify(line));
|
||||
if (newLines[0][2] > removedLayer) {
|
||||
newLines[0][2] -= 1;
|
||||
newLines[1][2] -= 1;
|
||||
}
|
||||
|
||||
const matchingLine = floorPlanGroupLine.current.children.find(
|
||||
(l: any) =>
|
||||
l.userData.linePoints[0][1] === line[0][1] &&
|
||||
l.userData.linePoints[1][1] === line[1][1]
|
||||
);
|
||||
if (matchingLine) {
|
||||
const updatedUserData = JSON.parse(
|
||||
JSON.stringify(matchingLine.userData)
|
||||
);
|
||||
updatedUserData.linePoints[0][2] = newLines[0][2];
|
||||
updatedUserData.linePoints[1][2] = newLines[1][2];
|
||||
matchingLine.userData = updatedUserData;
|
||||
}
|
||||
updatedLines.push(newLines);
|
||||
});
|
||||
|
||||
lines.current = updatedLines;
|
||||
localStorage.setItem("Lines", JSON.stringify(lines.current));
|
||||
|
||||
////////// Also remove OnlyFloorLines and update it in localstorage //////////
|
||||
|
||||
onlyFloorlines.current = onlyFloorlines.current.filter((floor: any) => {
|
||||
return floor[0][0][2] !== removedLayer;
|
||||
});
|
||||
const meshToRemove = floorGroup.current?.children.find(
|
||||
(mesh: any) => mesh.name === `Only_Floor_Line_${removedLayer}`
|
||||
);
|
||||
if (meshToRemove) {
|
||||
meshToRemove.geometry.dispose();
|
||||
meshToRemove.material.dispose();
|
||||
floorGroup.current?.remove(meshToRemove);
|
||||
}
|
||||
|
||||
const zonesData = await getZonesApi(organization, projectId);
|
||||
const highestLayer = Math.max(
|
||||
1,
|
||||
lines.current.reduce(
|
||||
(maxLayer: number, segment: any) =>
|
||||
Math.max(maxLayer, segment.layer || 0),
|
||||
0
|
||||
),
|
||||
zonesData.reduce(
|
||||
(maxLayer: number, zone: any) =>
|
||||
Math.max(maxLayer, zone.layer || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
setLayers(highestLayer);
|
||||
|
||||
loadWalls(lines, setWalls);
|
||||
setUpdateScene(true);
|
||||
|
||||
echo.success("Layer Removed!");
|
||||
}
|
||||
});
|
||||
}, [socket]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
|
||||
|
||||
socket.on("v1:wallItem:Response:Delete", (data: any) => {
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "wallitem deleted") {
|
||||
const deletedUUID = data.data.modelUuid;
|
||||
let WallItemsRef = wallItems;
|
||||
const Items = WallItemsRef.filter((item: any) => item.model?.uuid !== deletedUUID);
|
||||
|
||||
setWallItems([]);
|
||||
setTimeout(async () => {
|
||||
WallItemsRef = Items;
|
||||
setWallItems(WallItemsRef);
|
||||
const WallItemsForStorage = WallItemsRef.map((item: any) => {
|
||||
const { model, ...rest } = item;
|
||||
return {
|
||||
...rest,
|
||||
modelUuid: model?.uuid,
|
||||
};
|
||||
});
|
||||
|
||||
localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage));
|
||||
echo.success("Model Removed!");
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("v1:wallItems:Response:Update", (data: any) => {
|
||||
//
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "wallIitem created") {
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
|
||||
dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/');
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
// Check THREE.js cache first
|
||||
const cachedModel = THREE.Cache.get(data.data.modelfileID);
|
||||
if (cachedModel) {
|
||||
handleModelLoad(cachedModel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check IndexedDB cache
|
||||
retrieveGLTF(data.data.modelfileID).then((cachedModelBlob) => {
|
||||
if (cachedModelBlob) {
|
||||
const blobUrl = URL.createObjectURL(cachedModelBlob);
|
||||
loader.load(blobUrl, (gltf) => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
THREE.Cache.remove(blobUrl);
|
||||
THREE.Cache.add(data.data.modelfileID, gltf);
|
||||
handleModelLoad(gltf);
|
||||
});
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
// Load from backend if not in any cache
|
||||
loader.load(`${url_Backend_dwinzo}/api/v2/AssetFile/${data.data.modelfileID}`, async (gltf) => {
|
||||
try {
|
||||
const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v2/AssetFile/${data.data.modelfileID}`).then((res) => res.blob());
|
||||
await storeGLTF(data.data.modelfileID, modelBlob);
|
||||
THREE.Cache.add(data.data.modelfileID, gltf);
|
||||
await handleModelLoad(gltf);
|
||||
} catch (error) {
|
||||
|
||||
handleModelLoad(gltf);
|
||||
}
|
||||
});
|
||||
|
||||
async function handleModelLoad(gltf: GLTF) {
|
||||
const model = gltf.scene.clone();
|
||||
model.uuid = data.data.modelUuid;
|
||||
|
||||
model.children[0].children.forEach((child) => {
|
||||
if (child.name !== "CSG_REF") {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
}
|
||||
});
|
||||
|
||||
const newWallItem = {
|
||||
type: data.data.type,
|
||||
model: model,
|
||||
modelName: data.data.modelName,
|
||||
modelfileID: data.data.modelfileID,
|
||||
scale: data.data.scale,
|
||||
csgscale: data.data.csgscale,
|
||||
csgposition: data.data.csgposition,
|
||||
position: data.data.position,
|
||||
quaternion: data.data.quaternion,
|
||||
};
|
||||
|
||||
setWallItems((prevItems: Types.wallItems) => {
|
||||
const updatedItems = [...prevItems, newWallItem];
|
||||
|
||||
const WallItemsForStorage = updatedItems.map(item => {
|
||||
const { model, ...rest } = item;
|
||||
return {
|
||||
...rest,
|
||||
modelUuid: model?.uuid,
|
||||
};
|
||||
});
|
||||
|
||||
localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage));
|
||||
echo.success("Model Added!");
|
||||
return updatedItems;
|
||||
});
|
||||
}
|
||||
|
||||
} else if (data.message === "wallIitem updated") {
|
||||
const updatedUUID = data.data.modelUuid;
|
||||
|
||||
setWallItems((prevItems: any) => {
|
||||
const updatedItems = prevItems.map((item: any) => {
|
||||
if (item.model.uuid === updatedUUID) {
|
||||
return {
|
||||
...item,
|
||||
position: data.data.position,
|
||||
quaternion: data.data.quaternion,
|
||||
scale: data.data.scale,
|
||||
csgscale: data.data.csgscale,
|
||||
csgposition: data.data.csgposition,
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
const WallItemsForStorage = updatedItems.map((item: any) => {
|
||||
const { model, ...rest } = item;
|
||||
return {
|
||||
...rest,
|
||||
modelUuid: model?.uuid,
|
||||
};
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"WallItems",
|
||||
JSON.stringify(WallItemsForStorage)
|
||||
);
|
||||
echo.success("Model Updated!");
|
||||
|
||||
return updatedItems;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("v1:wallItem:Response:Delete");
|
||||
socket.off("v1:wallItems:Response:Update");
|
||||
};
|
||||
}, [wallItems]);
|
||||
|
||||
function getPointColor(lineType: string | undefined): string {
|
||||
switch (lineType) {
|
||||
case CONSTANTS.lineConfig.wallName:
|
||||
return CONSTANTS.pointConfig.wallOuterColor;
|
||||
case CONSTANTS.lineConfig.floorName:
|
||||
return CONSTANTS.pointConfig.floorOuterColor;
|
||||
case CONSTANTS.lineConfig.aisleName:
|
||||
return CONSTANTS.pointConfig.aisleOuterColor;
|
||||
default:
|
||||
return CONSTANTS.pointConfig.defaultOuterColor;
|
||||
}
|
||||
}
|
||||
|
||||
function getLineColor(lineType: string | undefined): string {
|
||||
switch (lineType) {
|
||||
case CONSTANTS.lineConfig.wallName:
|
||||
return CONSTANTS.lineConfig.wallColor;
|
||||
case CONSTANTS.lineConfig.floorName:
|
||||
return CONSTANTS.lineConfig.floorColor;
|
||||
case CONSTANTS.lineConfig.aisleName:
|
||||
return CONSTANTS.lineConfig.aisleColor;
|
||||
default:
|
||||
return CONSTANTS.lineConfig.defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
socket.on("v1:Line:response:create", async (data: any) => {
|
||||
//
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "line create") {
|
||||
const line: Types.Line = objectLineToArray(data.data);
|
||||
const type = line[0][3];
|
||||
const pointColour = getPointColor(type);
|
||||
const lineColour = getLineColor(type);
|
||||
setNewLines([line]);
|
||||
|
||||
line.forEach((line) => {
|
||||
const existingPoint =
|
||||
floorPlanGroupPoint.current?.getObjectByProperty("uuid", line[1]);
|
||||
if (existingPoint) {
|
||||
return;
|
||||
}
|
||||
const geometry = new THREE.BoxGeometry(
|
||||
...CONSTANTS.pointConfig.boxScale
|
||||
);
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
uOuterColor: { value: new THREE.Color(pointColour) }, // Blue color for the border
|
||||
uInnerColor: {
|
||||
value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor),
|
||||
}, // White color for the inner square
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec2 vUv;
|
||||
uniform vec3 uOuterColor;
|
||||
uniform vec3 uInnerColor;
|
||||
|
||||
void main() {
|
||||
// Define the size of the white square as a proportion of the face
|
||||
float borderThickness = 0.2; // Adjust this value for border thickness
|
||||
if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness &&
|
||||
vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) {
|
||||
gl_FragColor = vec4(uInnerColor, 1.0); // White inner square
|
||||
} else {
|
||||
gl_FragColor = vec4(uOuterColor, 1.0); // Blue border
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
const point = new THREE.Mesh(geometry, material);
|
||||
point.name = "point";
|
||||
point.uuid = line[1];
|
||||
point.userData = { type: type, color: pointColour };
|
||||
point.position.set(line[0].x, line[0].y, line[0].z);
|
||||
currentLayerPoint.current.push(point);
|
||||
|
||||
floorPlanGroupPoint.current?.add(point);
|
||||
});
|
||||
if (dragPointControls.current) {
|
||||
dragPointControls.current!.objects = currentLayerPoint.current;
|
||||
}
|
||||
addLineToScene(
|
||||
new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z),
|
||||
new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z),
|
||||
lineColour,
|
||||
line,
|
||||
floorPlanGroupLine
|
||||
);
|
||||
lines.current.push(line);
|
||||
|
||||
const zonesData = await getZonesApi(organization, projectId);
|
||||
const highestLayer = Math.max(
|
||||
1,
|
||||
lines.current.reduce(
|
||||
(maxLayer: number, segment: any) =>
|
||||
Math.max(maxLayer, segment.layer || 0),
|
||||
0
|
||||
),
|
||||
zonesData.reduce(
|
||||
(maxLayer: number, zone: any) =>
|
||||
Math.max(maxLayer, zone.layer || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
setLayers(highestLayer);
|
||||
|
||||
Layer2DVisibility(
|
||||
activeLayer,
|
||||
floorPlanGroup,
|
||||
floorPlanGroupLine,
|
||||
floorPlanGroupPoint,
|
||||
currentLayerPoint,
|
||||
dragPointControls
|
||||
);
|
||||
|
||||
loadWalls(lines, setWalls);
|
||||
setUpdateScene(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("v1:Line:response:create");
|
||||
};
|
||||
}, [socket, activeLayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const email = localStorage.getItem("email");
|
||||
const organization = email!.split("@")[1].split(".")[0];
|
||||
|
||||
socket.on("v1:zone:response:updates", (data: any) => {
|
||||
// console.log('data: ', data);
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.message === "zone created") {
|
||||
const pointsArray: [number, number, number][] = data.data.points;
|
||||
const vector3Array = pointsArray.map(
|
||||
([x, y, z]) => new THREE.Vector3(x, y, z)
|
||||
);
|
||||
const newZones = [...zones, data.data];
|
||||
setZones(newZones);
|
||||
const updatedZonePoints = [...zonePoints, ...vector3Array];
|
||||
setZonePoints(updatedZonePoints);
|
||||
|
||||
const highestLayer = Math.max(
|
||||
1,
|
||||
lines.current.reduce(
|
||||
(maxLayer: number, segment: any) =>
|
||||
Math.max(maxLayer, segment.layer || 0),
|
||||
0
|
||||
),
|
||||
newZones.reduce(
|
||||
(maxLayer: number, zone: any) =>
|
||||
Math.max(maxLayer, zone.layer || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
setLayers(highestLayer);
|
||||
setUpdateScene(true);
|
||||
}
|
||||
|
||||
if (data.message === "zone updated") {
|
||||
const updatedZones = zones.map((zone: any) =>
|
||||
zone.zoneUuid === data.data.zoneUuid ? data.data : zone
|
||||
);
|
||||
setZones(updatedZones);
|
||||
setUpdateScene(true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("v1:zone:response:delete", (data: any) => {
|
||||
// console.log('data: ', data);
|
||||
if (socket.id === data.socketId) {
|
||||
return;
|
||||
}
|
||||
if (organization !== data.organization) {
|
||||
return;
|
||||
}
|
||||
if (data.message === "zone deleted") {
|
||||
const updatedZones = zones.filter(
|
||||
(zone: any) => zone.zoneUuid !== data.data.zoneUuid
|
||||
);
|
||||
setZones(updatedZones);
|
||||
|
||||
const zoneIndex = zones.findIndex(
|
||||
(zone: any) => zone.zoneUuid === data.data.zoneUuid
|
||||
);
|
||||
if (zoneIndex !== -1) {
|
||||
const updatedzonePoints = zonePoints.filter(
|
||||
(_: any, index: any) =>
|
||||
index < zoneIndex * 4 || index >= zoneIndex * 4 + 4
|
||||
);
|
||||
setZonePoints(updatedzonePoints);
|
||||
}
|
||||
|
||||
const highestLayer = Math.max(
|
||||
1,
|
||||
lines.current.reduce(
|
||||
(maxLayer: number, segment: any) =>
|
||||
Math.max(maxLayer, segment.layer || 0),
|
||||
0
|
||||
),
|
||||
updatedZones.reduce(
|
||||
(maxLayer: number, zone: any) =>
|
||||
Math.max(maxLayer, zone.layer || 0),
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
setLayers(highestLayer);
|
||||
setUpdateScene(true);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.off("v1:zone:response:updates");
|
||||
socket.off("v1:zone:response:delete");
|
||||
};
|
||||
}, [socket, zones, zonePoints]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
58
app/src/modules/collaboration/users/Avatar.tsx
Normal file
58
app/src/modules/collaboration/users/Avatar.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getInitials } from "../functions/getInitials";
|
||||
|
||||
interface AvatarProps {
|
||||
name: string; // Name can be a full name or initials
|
||||
size?: number;
|
||||
textColor?: string;
|
||||
color?: string; // Optional color prop for future use
|
||||
}
|
||||
|
||||
const CustomAvatar: React.FC<AvatarProps> = ({
|
||||
name,
|
||||
size = 100,
|
||||
textColor = "#ffffff",
|
||||
color, // Optional color prop for future use
|
||||
}) => {
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = document.createElement("canvas"); // Create an offscreen canvas
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
const initials = getInitials(name); // Convert name to initials if needed
|
||||
|
||||
// Draw background
|
||||
ctx.fillStyle = color ?? "#323232"; // Use color prop or generate color based on index
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
|
||||
// Draw initials
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.font = `bold ${size / 2}px Arial`;
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(initials, size / 2, size / 2);
|
||||
|
||||
// Generate image source
|
||||
const dataURL = canvas.toDataURL("image/png");
|
||||
setImageSrc(dataURL);
|
||||
}
|
||||
}, [color, name, size, textColor]);
|
||||
|
||||
if (!imageSrc) {
|
||||
return null; // Return null while the image is being generated
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className="user-image"
|
||||
src={imageSrc}
|
||||
alt="User Avatar"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomAvatar;
|
||||
Reference in New Issue
Block a user