Merge branch 'v2-ui' of http://185.100.212.76:7776/Dwinzo-Beta/Dwinzo_dev into v2-ui
This commit is contained in:
commit
3dcff0976a
|
@ -4,8 +4,8 @@ import { useActiveUsers, useCamMode } from "../../../store/builder/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";
|
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
|
||||||
import {useToggleStore} from "../../../store/useUIToggleStore";
|
import { useToggleStore } from "../../../store/useUIToggleStore";
|
||||||
import { ToggleSidebarIcon } from "../../icons/HeaderIcons";
|
import { ToggleSidebarIcon } from "../../icons/HeaderIcons";
|
||||||
import useModuleStore from "../../../store/useModuleStore";
|
import useModuleStore from "../../../store/useModuleStore";
|
||||||
|
|
||||||
|
@ -62,9 +62,8 @@ const Header: React.FC = () => {
|
||||||
<div className="options-container">
|
<div className="options-container">
|
||||||
<button
|
<button
|
||||||
id="toggle-rightSidebar-ui-button"
|
id="toggle-rightSidebar-ui-button"
|
||||||
className={`toggle-sidebar-ui-button ${
|
className={`toggle-sidebar-ui-button ${!toggleUIRight ? "active" : ""
|
||||||
!toggleUIRight ? "active" : ""
|
}`}
|
||||||
}`}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (activeModule !== "market") {
|
if (activeModule !== "market") {
|
||||||
setToggleUI(toggleUILeft, !toggleUIRight);
|
setToggleUI(toggleUILeft, !toggleUIRight);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import RenderOverlay from "./Overlay";
|
import RenderOverlay from "./Overlay";
|
||||||
import { useSelectedUserStore } from "../../store/useCollabStore";
|
import { useSelectedUserStore } from "../../store/collaboration/useCollabStore";
|
||||||
import { useCamMode } from "../../store/builder/store";
|
import { useCamMode } from "../../store/builder/store";
|
||||||
|
|
||||||
const FollowPerson: React.FC = () => {
|
const FollowPerson: React.FC = () => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ const CommentThreads: React.FC = () => {
|
||||||
userName: "user",
|
userName: "user",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
userId: "1",
|
userId: "2",
|
||||||
userName: "Admin",
|
userName: "Admin",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -28,7 +28,9 @@ const CommentThreads: React.FC = () => {
|
||||||
onPointerEnter={() => getDetails()}
|
onPointerEnter={() => getDetails()}
|
||||||
onPointerLeave={() => getDetails()}
|
onPointerLeave={() => getDetails()}
|
||||||
onClick={() => getDetails("clicked")}
|
onClick={() => getDetails("clicked")}
|
||||||
className={`comments-threads-container ${expand ? "open" : "closed"}`}
|
className={`comments-threads-container ${
|
||||||
|
expand ? "open" : "closed"
|
||||||
|
} unread`}
|
||||||
>
|
>
|
||||||
<div className="users-commented">
|
<div className="users-commented">
|
||||||
{commentsedUsers.map((val, i) => (
|
{commentsedUsers.map((val, i) => (
|
||||||
|
@ -41,9 +43,7 @@ const CommentThreads: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={`last-comment-details ${expand ? "expand" : ""}`}>
|
||||||
className={`last-comment-details ${expand ? "expand" : ""}`}
|
|
||||||
>
|
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<div className="user-name">user</div>
|
<div className="user-name">user</div>
|
||||||
<div className="time">4 mins, ago</div>
|
<div className="time">4 mins, ago</div>
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor";
|
||||||
|
import { KebabIcon } from "../../icons/ExportCommonIcons";
|
||||||
|
|
||||||
|
interface MessageProps {
|
||||||
|
val: {
|
||||||
|
userName: string;
|
||||||
|
userId: string;
|
||||||
|
message: string;
|
||||||
|
creationTime: string;
|
||||||
|
idEdited: boolean;
|
||||||
|
modifiedTime: string;
|
||||||
|
};
|
||||||
|
i: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Messages: React.FC<MessageProps> = ({ val, i }) => {
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [openOptions, setOpenOptions] = useState(false);
|
||||||
|
const currentUser = "1";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isEditing ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="profile"
|
||||||
|
style={{ background: getAvatarColor(i, val.userName) }}
|
||||||
|
>
|
||||||
|
{val.userName[0]}
|
||||||
|
</div>
|
||||||
|
<div className="content">
|
||||||
|
<div className="user-details">
|
||||||
|
<div className="user-name">{val.userName}</div>
|
||||||
|
<div className="time">{val.creationTime}</div>
|
||||||
|
</div>
|
||||||
|
{val.userId === currentUser && (
|
||||||
|
<div className="more-options">
|
||||||
|
<button
|
||||||
|
className="more-options-button"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenOptions(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KebabIcon />
|
||||||
|
</button>
|
||||||
|
{openOptions && (
|
||||||
|
<div className="options-list">
|
||||||
|
<button
|
||||||
|
className="option"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button className="option">Delete</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="edit-container">
|
||||||
|
<div className="input-container">
|
||||||
|
<textarea />
|
||||||
|
</div>
|
||||||
|
<div className="actions-container">
|
||||||
|
<div className="actions">
|
||||||
|
<button className="cancel-button">Cancel</button>
|
||||||
|
<button className="save-button">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Messages;
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React from "react";
|
||||||
|
import { CloseIcon, KebabIcon } from "../../icons/ExportCommonIcons";
|
||||||
|
import Messages from "./Messages";
|
||||||
|
import { ExpandIcon } from "../../icons/SimulationIcons";
|
||||||
|
|
||||||
|
const ThreadChat: React.FC = () => {
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
userName: "user 1",
|
||||||
|
userId: "1",
|
||||||
|
message: "hello, thread check",
|
||||||
|
creationTime: "2 hrs ago",
|
||||||
|
idEdited: true,
|
||||||
|
modifiedTime: "5 mins ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userName: "user 2",
|
||||||
|
userId: "2",
|
||||||
|
message: "hello, thread check",
|
||||||
|
creationTime: "2 hrs ago",
|
||||||
|
idEdited: true,
|
||||||
|
modifiedTime: "5 mins ago",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="thread-char-wrapper">
|
||||||
|
<div className="thread-char-container">
|
||||||
|
<div className="header-wrapper">
|
||||||
|
<div className="header">Comment</div>
|
||||||
|
<div className="header-options">
|
||||||
|
<button className="options-button">
|
||||||
|
<KebabIcon />
|
||||||
|
</button>
|
||||||
|
<div className="options-list">
|
||||||
|
<div className="options">Mark as Unread</div>
|
||||||
|
<div className="options">Mark as Resolved</div>
|
||||||
|
<div className="options">Delete Thread</div>
|
||||||
|
</div>
|
||||||
|
<button className="close-button">
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="messages-wrapper">
|
||||||
|
{messages.map((val, i) => (
|
||||||
|
<Messages val={val} i={i} key={val.userId} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="send-message-wrapper">
|
||||||
|
<div className="input-container">
|
||||||
|
<input type="text" />
|
||||||
|
<div className="sent-button">
|
||||||
|
<ExpandIcon />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThreadChat;
|
|
@ -1,18 +1,19 @@
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useEffect, useRef, useState } from "react";
|
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 { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
|
||||||
import camModel from "../../../assets/gltf-glb/camera face 2.gltf";
|
import camModel from "../../../assets/gltf-glb/camera face 2.gltf";
|
||||||
import getActiveUsersData from "../../../services/factoryBuilder/collab/getActiveUsers";
|
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 { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Html } from "@react-three/drei";
|
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";
|
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
|
||||||
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
import { usePlayButtonStore } from "../../../store/usePlayButtonStore";
|
||||||
|
import setCameraView from "../functions/setCameraView";
|
||||||
|
|
||||||
const CamModelsGroup = () => {
|
const CamModelsGroup = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -30,6 +31,28 @@ const CamModelsGroup = () => {
|
||||||
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
|
dracoLoader.setDecoderPath("three/examples/jsm/libs/draco/gltf/");
|
||||||
loader.setDRACOLoader(dracoLoader);
|
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 [cams, setCams] = useState<any[]>([]);
|
||||||
const [models, setModels] = useState<
|
const [models, setModels] = useState<
|
||||||
Record<
|
Record<
|
||||||
|
@ -260,11 +283,10 @@ const CamModelsGroup = () => {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
fontFamily: "Arial, sans-serif",
|
fontFamily: "Arial, sans-serif",
|
||||||
display: `${activeModule !== "visualization" ? "" : "none"}`,
|
display: `${activeModule !== "visualization" ? "" : "none"}`,
|
||||||
opacity: `${
|
opacity: `${selectedUser?.name !== cam.userData.userName && !isPlaying
|
||||||
selectedUser?.name !== cam.userData.userName && !isPlaying
|
? 1
|
||||||
? 1
|
: 0
|
||||||
: 0
|
}`,
|
||||||
}`,
|
|
||||||
transition: "opacity .2s ease",
|
transition: "opacity .2s ease",
|
||||||
}}
|
}}
|
||||||
position={[-0.015, 0, 0.7]}
|
position={[-0.015, 0, 0.7]}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import CustomAvatar from "../users/Avatar";
|
import CustomAvatar from "../users/Avatar";
|
||||||
import { useSelectedUserStore } from "../../../store/useCollabStore";
|
import { useSelectedUserStore } from "../../../store/collaboration/useCollabStore";
|
||||||
import { useCamMode } from "../../../store/builder/store";
|
import { useCamMode } from "../../../store/builder/store";
|
||||||
|
|
||||||
interface CollabUserIconProps {
|
interface CollabUserIconProps {
|
||||||
|
|
|
@ -1,34 +1,18 @@
|
||||||
import React, { useEffect } from "react";
|
import React from "react";
|
||||||
import CamModelsGroup from "./camera/collabCams";
|
import CamModelsGroup from "./camera/collabCams";
|
||||||
import { useSelectedUserStore } from "../../store/useCollabStore";
|
import CommentsGroup from "./comments/commentsGroup";
|
||||||
import { useThree } from "@react-three/fiber";
|
|
||||||
import setCameraView from "./functions/setCameraView";
|
|
||||||
import { useCamMode } from "../../store/builder/store";
|
|
||||||
|
|
||||||
const Collaboration: React.FC = () => {
|
const Collaboration: React.FC = () => {
|
||||||
const { selectedUser } = useSelectedUserStore();
|
|
||||||
const { camMode } = useCamMode();
|
|
||||||
const { camera, controls } = useThree(); // Access R3F camera and controls
|
|
||||||
|
|
||||||
useEffect(() => {
|
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, target } = selectedUser.location;
|
|
||||||
if (rotation && target)
|
|
||||||
setCameraView({
|
|
||||||
controls,
|
|
||||||
camera,
|
|
||||||
position,
|
|
||||||
rotation,
|
|
||||||
target,
|
|
||||||
username: selectedUser.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [selectedUser, camera, controls, camMode]);
|
|
||||||
|
|
||||||
return <CamModelsGroup />;
|
<CamModelsGroup />
|
||||||
|
|
||||||
|
<CommentsGroup />
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Collaboration;
|
export default Collaboration;
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
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(),
|
||||||
|
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,74 @@
|
||||||
|
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]);
|
||||||
|
|
||||||
|
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 />
|
||||||
|
{/* <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;
|
|
@ -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
|
|
@ -1,6 +1,7 @@
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import { useMemo, useRef } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { useThree } from "@react-three/fiber";
|
import { useThree } from "@react-three/fiber";
|
||||||
|
import { useDeleteTool } from "../../../../store/builder/store";
|
||||||
|
|
||||||
interface ConnectionLine {
|
interface ConnectionLine {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -10,8 +11,10 @@ interface ConnectionLine {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Arrows({ connections }: { connections: ConnectionLine[] }) {
|
export function Arrows({ connections }: { connections: ConnectionLine[] }) {
|
||||||
|
const [hoveredLineKey, setHoveredLineKey] = useState<string | null>(null);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const { scene } = useThree();
|
const { scene } = useThree();
|
||||||
|
const { deleteTool } = useDeleteTool();
|
||||||
|
|
||||||
const getWorldPositionFromScene = (uuid: string): THREE.Vector3 | null => {
|
const getWorldPositionFromScene = (uuid: string): THREE.Vector3 | null => {
|
||||||
const obj = scene.getObjectByProperty("uuid", uuid);
|
const obj = scene.getObjectByProperty("uuid", uuid);
|
||||||
|
@ -52,11 +55,25 @@ export function Arrows({ connections }: { connections: ConnectionLine[] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group key={key}>
|
<group key={key}>
|
||||||
<mesh geometry={shaftGeometry}>
|
<mesh
|
||||||
<meshStandardMaterial color="#42a5f5" />
|
geometry={shaftGeometry}
|
||||||
|
onPointerOver={() => setHoveredLineKey(key)}
|
||||||
|
onPointerOut={() => setHoveredLineKey(null)}
|
||||||
|
>
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={end} quaternion={rotation} geometry={headGeometry}>
|
<mesh
|
||||||
<meshStandardMaterial color="#42a5f5" />
|
position={end}
|
||||||
|
quaternion={rotation}
|
||||||
|
geometry={headGeometry}
|
||||||
|
onPointerOver={() => setHoveredLineKey(key)}
|
||||||
|
onPointerOut={() => setHoveredLineKey(null)}
|
||||||
|
>
|
||||||
|
<meshStandardMaterial
|
||||||
|
color={deleteTool && hoveredLineKey === key ? "red" : "#42a5f5"}
|
||||||
|
/>
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { usePlayButtonStore } from "../store/usePlayButtonStore";
|
||||||
import MarketPlace from "../modules/market/MarketPlace";
|
import MarketPlace from "../modules/market/MarketPlace";
|
||||||
import LoadingPage from "../components/templates/LoadingPage";
|
import LoadingPage from "../components/templates/LoadingPage";
|
||||||
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
|
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
|
||||||
import { useSelectedUserStore } from "../store/useCollabStore";
|
import { useSelectedUserStore } from "../store/collaboration/useCollabStore";
|
||||||
import FollowPerson from "../components/templates/FollowPerson";
|
import FollowPerson from "../components/templates/FollowPerson";
|
||||||
import Scene from "../modules/scene/scene";
|
import Scene from "../modules/scene/scene";
|
||||||
import { createHandleDrop } from "../modules/visualization/functions/handleUiDrop";
|
import { createHandleDrop } from "../modules/visualization/functions/handleUiDrop";
|
||||||
|
@ -34,7 +34,7 @@ import Footer from "../components/footer/Footer";
|
||||||
import SelectFloorPlan from "../components/temporary/SelectFloorPlan";
|
import SelectFloorPlan from "../components/temporary/SelectFloorPlan";
|
||||||
import ControlsPlayer from "../components/layout/controls/ControlsPlayer";
|
import ControlsPlayer from "../components/layout/controls/ControlsPlayer";
|
||||||
import CompareLayOut from "../components/ui/compareVersion/CompareLayOut";
|
import CompareLayOut from "../components/ui/compareVersion/CompareLayOut";
|
||||||
import {useToggleStore} from "../store/useUIToggleStore";
|
import { useToggleStore } from "../store/useUIToggleStore";
|
||||||
import RegularDropDown from "../components/ui/inputs/RegularDropDown";
|
import RegularDropDown from "../components/ui/inputs/RegularDropDown";
|
||||||
import VersionSaved from "../components/layout/sidebarRight/versionHisory/VersionSaved";
|
import VersionSaved from "../components/layout/sidebarRight/versionHisory/VersionSaved";
|
||||||
import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
|
import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
|
||||||
|
@ -183,7 +183,6 @@ const Project: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<VersionSaved />
|
<VersionSaved />
|
||||||
<CommentThreads />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,36 +1,36 @@
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
interface SelectedUser {
|
interface SelectedUser {
|
||||||
color: string;
|
color: string;
|
||||||
name: string;
|
name: string;
|
||||||
id: string,
|
id: string,
|
||||||
location?: {
|
location?: {
|
||||||
position: {
|
position: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
};
|
};
|
||||||
rotation?: {
|
rotation?: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
};
|
};
|
||||||
target?: {
|
target?: {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
z: number;
|
z: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectedUserStore {
|
interface SelectedUserStore {
|
||||||
selectedUser: SelectedUser | null;
|
selectedUser: SelectedUser | null;
|
||||||
setSelectedUser: (user: SelectedUser) => void;
|
setSelectedUser: (user: SelectedUser) => void;
|
||||||
clearSelectedUser: () => void;
|
clearSelectedUser: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelectedUserStore = create<SelectedUserStore>((set) => ({
|
export const useSelectedUserStore = create<SelectedUserStore>((set) => ({
|
||||||
selectedUser: null,
|
selectedUser: null,
|
||||||
setSelectedUser: (user) => set({ selectedUser: user }),
|
setSelectedUser: (user) => set({ selectedUser: user }),
|
||||||
clearSelectedUser: () => set({ selectedUser: null }),
|
clearSelectedUser: () => set({ selectedUser: null }),
|
||||||
}));
|
}));
|
|
@ -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);
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
);
|
|
@ -1,10 +1,13 @@
|
||||||
@use "../abstracts/variables" as *;
|
@use "../abstracts/variables" as *;
|
||||||
@use "../abstracts/mixins" as *;
|
@use "../abstracts/mixins" as *;
|
||||||
|
|
||||||
|
.comments-main-wrapper{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.comments-threads-wrapper {
|
.comments-threads-wrapper {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
top: 50%;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
border-radius: #{$border-radius-large} #{$border-radius-large} #{$border-radius-large} 0;
|
border-radius: #{$border-radius-large} #{$border-radius-large} #{$border-radius-large} 0;
|
||||||
|
@ -35,9 +38,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
// padding-top: 0;
|
|
||||||
// height: 0;
|
|
||||||
// width: 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
@ -60,16 +60,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.replies {
|
.replies {
|
||||||
margin-top: 10px;
|
margin-top: 4px;
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
color: var(--input-text-color);
|
color: var(--input-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header,
|
.header,
|
||||||
.message,
|
.message,
|
||||||
.replies {
|
.replies {
|
||||||
opacity: 0;
|
|
||||||
display: none;
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,6 +76,7 @@
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 260px;
|
max-width: 260px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
padding-top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,13 +85,11 @@
|
||||||
.users-commented {
|
.users-commented {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.header,
|
.header,
|
||||||
.message,
|
.message,
|
||||||
.replies {
|
.replies {
|
||||||
opacity: 1 !important;
|
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
interface CommentSchema {
|
||||||
|
state: "active" | "inactive";
|
||||||
|
commentId: string;
|
||||||
|
creatorId: string;
|
||||||
|
createdAt: string;
|
||||||
|
comment: 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[];
|
Loading…
Reference in New Issue