Refactor collaboration store structure and implement comments functionality

This commit is contained in:
Jerald-Golden-B 2025-05-22 15:44:13 +05:30
parent 70136fd7d3
commit 60e47f9764
13 changed files with 431 additions and 79 deletions

View File

@ -4,7 +4,7 @@ import { useActiveUsers, useCamMode } from "../../../store/builder/store";
import { ActiveUser } from "../../../types/users";
import CollaborationPopup from "../../templates/CollaborationPopup";
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
import { useSelectedUserStore } from "../../../store/useCollabStore";
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
import useToggleStore from "../../../store/useUIToggleStore";
import { ToggleSidebarIcon } from "../../icons/HeaderIcons";
import useModuleStore from "../../../store/useModuleStore";

View File

@ -1,6 +1,6 @@
import React from "react";
import RenderOverlay from "./Overlay";
import { useSelectedUserStore } from "../../store/useCollabStore";
import { useSelectedUserStore } from "../../store/collaboration/useCollabStore";
import { useCamMode } from "../../store/builder/store";
const FollowPerson: React.FC = () => {

View File

@ -1,18 +1,19 @@
import * as THREE from "three";
import { useEffect, useRef, useState } from "react";
import { useFrame } from "@react-three/fiber";
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, useSocketStore } from "../../../store/builder/store";
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/useCollabStore";
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
import setCameraView from "../functions/setCameraView";
const CamModelsGroup = () => {
const navigate = useNavigate();
@ -30,6 +31,28 @@ const CamModelsGroup = () => {
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<
@ -260,11 +283,10 @@ const CamModelsGroup = () => {
textAlign: "center",
fontFamily: "Arial, sans-serif",
display: `${activeModule !== "visualization" ? "" : "none"}`,
opacity: `${
selectedUser?.name !== cam.userData.userName && !isPlaying
? 1
: 0
}`,
opacity: `${selectedUser?.name !== cam.userData.userName && !isPlaying
? 1
: 0
}`,
transition: "opacity .2s ease",
}}
position={[-0.015, 0, 0.7]}

View File

@ -1,6 +1,6 @@
import React from "react";
import CustomAvatar from "../users/Avatar";
import { useSelectedUserStore } from "../../../store/useCollabStore";
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
import { useCamMode } from "../../../store/builder/store";
interface CollabUserIconProps {

View File

@ -1,34 +1,18 @@
import React, { useEffect } from "react";
import React from "react";
import CamModelsGroup from "./camera/collabCams";
import { useSelectedUserStore } from "../../store/useCollabStore";
import { useThree } from "@react-three/fiber";
import setCameraView from "./functions/setCameraView";
import { useCamMode } from "../../store/builder/store";
import CommentsGroup from "./comments/commentsGroup";
const Collaboration: React.FC = () => {
const { selectedUser } = useSelectedUserStore();
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]);
return (
<>
return <CamModelsGroup />;
<CamModelsGroup />
<CommentsGroup />
</>
);
};
export default Collaboration;

View File

@ -0,0 +1,123 @@
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("commentHolder") &&
!intersect.object.name.includes("agv-collider") &&
!(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("commentHolder") &&
!intersect.object.name.includes("agv-collider") &&
!(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(),
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

View File

@ -0,0 +1,71 @@
import { Html, TransformControls } from '@react-three/drei';
import { useEffect, useRef, useState } from 'react'
import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
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);
if (comment.state === 'inactive' || isPlaying) return 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]);
return (
<>
<Html
ref={CommentRef}
zIndexRange={[1, 0]}
prepend
sprite
center
position={comment.position}
rotation={comment.rotation}
>
<div
className='outer'
onClick={(e) => {
e.stopPropagation();
setSelectedComment(comment);
console.log("down");
}}
onPointerOver={(e) => {
e.stopPropagation();
}}>
<div>
hii
</div>
</div>
</Html>
{CommentRef.current && transformMode && (
<TransformControls
object={CommentRef.current}
mode={transformMode}
onMouseUp={(e) => {
}}
/>
)}
</>
)
}
export default CommentInstance;

View File

@ -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

View File

@ -1,6 +1,7 @@
import * as THREE from "three";
import { useMemo, useRef } from "react";
import { useMemo, useRef, useState } from "react";
import { useThree } from "@react-three/fiber";
import { useDeleteTool } from "../../../../store/builder/store";
interface ConnectionLine {
id: string;
@ -10,8 +11,10 @@ interface ConnectionLine {
}
export function Arrows({ connections }: { connections: ConnectionLine[] }) {
const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null);
const groupRef = useRef<THREE.Group>(null);
const { scene } = useThree();
const { deleteTool } = useDeleteTool();
const getWorldPositionFromScene = (uuid: string): THREE.Vector3 | null => {
const obj = scene.getObjectByProperty("uuid", uuid);
@ -52,11 +55,25 @@ export function Arrows({ connections }: { connections: ConnectionLine[] }) {
return (
<group key={key}>
<mesh geometry={shaftGeometry}>
<meshStandardMaterial color="#42a5f5" />
<mesh
geometry={shaftGeometry}
onPointerOver={() => setHoveredLineKey(key)}
onPointerOut={() => setHoveredLineKey(null)}
>
<meshStandardMaterial
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
/>
</mesh>
<mesh position={end} quaternion={rotation} geometry={headGeometry}>
<meshStandardMaterial color="#42a5f5" />
<mesh
position={end}
quaternion={rotation}
geometry={headGeometry}
onPointerOver={() => setHoveredLineKey(key)}
onPointerOut={() => setHoveredLineKey(null)}
>
<meshStandardMaterial
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
/>
</mesh>
</group>
);

View File

@ -21,7 +21,7 @@ import { usePlayButtonStore } from "../store/usePlayButtonStore";
import MarketPlace from "../modules/market/MarketPlace";
import LoadingPage from "../components/templates/LoadingPage";
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
import { useSelectedUserStore } from "../store/useCollabStore";
import { useSelectedUserStore } from "../store/collaboration/useCollabStore";
import FollowPerson from "../components/templates/FollowPerson";
import Scene from "../modules/scene/scene";
import { createHandleDrop } from "../modules/visualization/functions/handleUiDrop";

View File

@ -1,36 +1,36 @@
import { create } from 'zustand';
interface SelectedUser {
color: string;
name: string;
id: string,
location?: {
position: {
x: number;
y: number;
z: number;
};
rotation?: {
x: number;
y: number;
z: number;
};
target?: {
x: number;
y: number;
z: number;
};
}
}
interface SelectedUserStore {
selectedUser: SelectedUser | null;
setSelectedUser: (user: SelectedUser) => void;
clearSelectedUser: () => void;
}
export const useSelectedUserStore = create<SelectedUserStore>((set) => ({
selectedUser: null,
setSelectedUser: (user) => set({ selectedUser: user }),
clearSelectedUser: () => set({ selectedUser: null }),
}));
import { create } from 'zustand';
interface SelectedUser {
color: string;
name: string;
id: string,
location?: {
position: {
x: number;
y: number;
z: number;
};
rotation?: {
x: number;
y: number;
z: number;
};
target?: {
x: number;
y: number;
z: number;
};
}
}
interface SelectedUserStore {
selectedUser: SelectedUser | null;
setSelectedUser: (user: SelectedUser) => void;
clearSelectedUser: () => void;
}
export const useSelectedUserStore = create<SelectedUserStore>((set) => ({
selectedUser: null,
setSelectedUser: (user) => set({ selectedUser: user }),
clearSelectedUser: () => set({ selectedUser: null }),
}));

View File

@ -0,0 +1,92 @@
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
interface CommentStore {
comments: CommentsSchema;
// Comment operations
addComment: (comment: CommentSchema) => void;
setComments: (comments: CommentsSchema) => void;
updateComment: (commentId: string, updates: Partial<CommentSchema>) => void;
removeComment: (commentId: string) => void;
// Reply operations
addReply: (commentId: string, reply: Reply) => void;
updateReply: (commentId: string, replyId: string, updates: Partial<Reply>) => void;
removeReply: (commentId: string, replyId: string) => void;
// Getters
getCommentById: (commentId: string) => CommentSchema | undefined;
}
export const useCommentStore = create<CommentStore>()(
immer((set, get) => ({
comments: [],
// Comment operations
addComment: (comment) => {
set((state) => {
if (!state.comments.find(c => c.commentId === comment.commentId)) {
state.comments.push(JSON.parse(JSON.stringify(comment)));
}
});
},
setComments: (comments) => {
set((state) => {
state.comments = comments;
});
},
updateComment: (commentId, updates) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
Object.assign(comment, updates);
}
});
},
removeComment: (commentId) => {
set((state) => {
state.comments = state.comments.filter(c => c.commentId !== commentId);
});
},
// Reply operations
addReply: (commentId, reply) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
comment.replies.push(reply);
}
});
},
updateReply: (commentId, replyId, updates) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
const reply = comment.replies.find(r => r.replyId === replyId);
if (reply) {
Object.assign(reply, updates);
}
}
});
},
removeReply: (commentId, replyId) => {
set((state) => {
const comment = state.comments.find(c => c.commentId === commentId);
if (comment) {
comment.replies = comment.replies.filter(r => r.replyId !== replyId);
}
});
},
// Getter
getCommentById: (commentId) => {
return get().comments.find(c => c.commentId === commentId);
},
}))
);

20
app/src/types/collaborationTypes.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
interface CommentSchema {
state: "active" | "inactive";
commentId: string;
creatorId: string;
createdAt: string;
lastUpdatedAt: string;
position: [number, number, number];
rotation: [number, number, number];
replies: Reply[];
}
interface Reply {
replyId: string;
creatorId: string;
createdAt: string;
lastUpdatedAt: string;
reply: string;
}
type CommentsSchema = CommentSchema[];