Compare commits

...

13 Commits

Author SHA1 Message Date
64d086f808 feat: Implement wall asset management APIs and socket integration for create, update, and delete operations 2025-07-02 10:49:07 +05:30
d6f6c4c901 Merge remote-tracking branch 'origin/dev-collaboration' into dev-r3f-wall 2025-06-30 18:13:23 +05:30
0a039f34b1 feat: Integrate active tool management across builder components; add deletableWallAsset state and related functionality 2025-06-30 18:11:37 +05:30
364b643c72 feat: Enhance wall asset interaction and management; implement position updates and add closest point calculation utility 2025-06-30 17:48:29 +05:30
1a9aef323a feat: Add selectedWallAsset and selectedFloorAsset state management; implement corresponding setters in useBuilderStore 2025-06-30 16:59:27 +05:30
997775c27e feat: Implement wall asset management features including creation, instances, and rendering; enhance wall properties input validation 2025-06-30 16:21:54 +05:30
b81aa10478 Capitalize userName in CollaborationPopup for consistent display 2025-06-27 09:49:20 +05:30
509f79db2c Enhance AssetProperties component with animation hover effects and styling improvements 2025-06-27 09:13:11 +05:30
d926809dec Add scene context and animation handling to AssetProperties and Model components
- Enhanced animation handling in Model component with animation state management.
- Updated useAssetStore to support multiple animations for assets.
2025-06-26 15:11:52 +05:30
e5e92d2b9f Merge remote-tracking branch 'origin/main-dev' into dev-collaboration 2025-06-26 10:59:22 +05:30
b3b0831a7f Enhance dashboard components with user collaboration features and project management improvements 2025-06-26 09:25:02 +05:30
62e315e3d9 Merge remote-tracking branch 'origin/main-dev' into dev-collaboration 2025-06-25 09:36:22 +05:30
13a2648e83 Implement collaboration features with user access management and email invitation functionality 2025-06-25 09:35:47 +05:30
45 changed files with 1601 additions and 249 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useEffect } from "react";
import React, { useState, useRef, useEffect, act } from "react";
import img from "../../assets/image/image.png";
import { useNavigate } from "react-router-dom";
import { getUserData } from "../../functions/getUserData";
@@ -14,13 +14,15 @@ interface DashBoardCardProps {
projectId: string;
createdAt?: string;
isViewed?: string;
createdBy?: { _id: string, userName: string };
handleDeleteProject?: (projectId: string) => Promise<void>;
handleTrashDeleteProject?: (projectId: string) => Promise<void>;
handleRestoreProject?: (projectId: string) => Promise<void>;
handleDuplicateWorkspaceProject?: (
projectId: string,
projectName: string,
thumbnail: string
thumbnail: string,
userId?: string
) => Promise<void>;
handleDuplicateRecentProject?: (
projectId: string,
@@ -31,6 +33,7 @@ interface DashBoardCardProps {
setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
setRecentDuplicateData?: React.Dispatch<React.SetStateAction<Object>>;
setProjectDuplicateData?: React.Dispatch<React.SetStateAction<Object>>;
setActiveFolder?: React.Dispatch<React.SetStateAction<string>>;
}
type RelativeTimeFormatUnit = any;
@@ -45,8 +48,10 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
createdAt,
createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder
}) => {
const navigate = useNavigate();
const { setProjectName } = useProjectName();
@@ -59,10 +64,18 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
const kebabRef = useRef<HTMLDivElement>(null);
const navigateToProject = async (e: any) => {
console.log('active: ', active);
if (active && active == "trash") return;
setLoadingProgress(1)
setProjectName(projectName);
navigate(`/${projectId}`);
try {
const viewProjects = await viewProject(organization, projectId, userId)
console.log('viewProjects: ', viewProjects);
console.log('projectName: ', projectName);
setLoadingProgress(1)
setProjectName(projectName);
navigate(`/${projectId}`);
} catch {
}
};
const handleOptionClick = async (option: string) => {
@@ -81,11 +94,18 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
break;
case "open in new tab":
try {
await viewProject(organization, projectId, userId);
setProjectName(projectName);
setIsKebabOpen(false);
if (active === "shared" && createdBy) {
console.log("ihreq");
const newTab = await viewProject(organization, projectId, createdBy?._id);
console.log('newTab: ', newTab);
} else {
const newTab = await viewProject(organization, projectId, userId);
console.log('newTab: ', newTab);
setProjectName(projectName);
setIsKebabOpen(false);
}
} catch (error) {
console.error("Error opening project in new tab:", error);
}
window.open(`/${projectId}`, "_blank");
break;
@@ -100,13 +120,17 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
projectName,
thumbnail,
});
await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail);
await handleDuplicateWorkspaceProject(projectId, projectName, thumbnail, userId);
if (active === "shared" && setActiveFolder) {
setActiveFolder("myProjects")
}
} else if (handleDuplicateRecentProject) {
setRecentDuplicateData &&
setRecentDuplicateData({
projectId,
projectName,
thumbnail,
userId
});
await handleDuplicateRecentProject(projectId, projectName, thumbnail);
}
@@ -128,7 +152,6 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
try {
const projects = await getAllProjects(userId, organization);
if (!projects || !projects.Projects) return;
// console.log("projects: ", projects);
let projectUuid = projects.Projects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
);
@@ -173,6 +196,19 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
return "just now";
}
const kebabOptionsMap: Record<string, string[]> = {
default: ["rename", "delete", "duplicate", "open in new tab"],
trash: ["restore", "delete"],
shared: ["duplicate", "open in new tab"],
};
const getOptions = () => {
if (active === "trash") return kebabOptionsMap.trash;
if (active === "shared") return kebabOptionsMap.shared;
if (createdBy && createdBy?._id !== userId) return kebabOptionsMap.shared;
return kebabOptionsMap.default;
};
return (
<button
className="dashboard-card-container"
@@ -212,13 +248,12 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
<div className="project-data">
{active && active == "trash" ? `Trashed by you` : `Edited `}{" "}
{getRelativeTime(createdAt)}
</div>
)}
</div>
<div className="users-list-container" ref={kebabRef}>
<div className="user-profile">
{userName ? userName.charAt(0).toUpperCase() : "A"}
{(!createdBy) ? userName ? userName?.charAt(0).toUpperCase() : "A" : createdBy?.userName?.charAt(0).toUpperCase()}
</div>
<button
className="kebab-wrapper"
@@ -232,29 +267,9 @@ const DashboardCard: React.FC<DashBoardCardProps> = ({
</div>
</div>
</div>
{isKebabOpen && active !== "trash" && (
{isKebabOpen && (
<div className="kebab-options-wrapper">
{["rename", "delete", "duplicate", "open in new tab"].map(
(option) => (
<button
key={option}
className="option"
title={""}
onClick={(e) => {
e.stopPropagation();
handleOptionClick(option);
}}
>
{option}
</button>
)
)}
</div>
)}
{isKebabOpen && active && active == "trash" && (
<div className="kebab-options-wrapper">
{["restore", "delete"].map((option) => (
{getOptions().map((option) => (
<button
key={option}
className="option"

View File

@@ -13,7 +13,7 @@ interface Project {
_id: string;
projectName: string;
thumbnail: string;
createdBy: string;
createdBy: { _id: string, userName: string };
projectUuid?: string;
createdAt: string;
isViewed?: string
@@ -34,7 +34,8 @@ const DashboardHome: React.FC = () => {
const fetchRecentProjects = async () => {
try {
const projects = await recentlyViewed(organization, userId);
console.log('projects: ', projects);
if (JSON.stringify(projects) !== JSON.stringify(recentProjects)) {
setRecentProjects(projects);
}
@@ -126,7 +127,8 @@ const DashboardHome: React.FC = () => {
projectName={project.projectName}
thumbnail={project.thumbnail}
projectId={project._id}
createdAt={project.isViewed}
createdBy={project.createdBy}
createdAt={project.createdAt}
handleDeleteProject={handleDeleteProject}
handleDuplicateRecentProject={handleDuplicateRecentProject}
setRecentDuplicateData={setRecentDuplicateData}

View File

@@ -7,6 +7,8 @@ import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { searchProject } from "../../services/dashboard/searchProjects";
import { deleteProject } from "../../services/dashboard/deleteProject";
import ProjectSocketRes from "./socket/projectSocketRes.dev";
import { sharedWithMeProjects } from "../../services/dashboard/sharedWithMeProject";
import { duplicateProject } from "../../services/dashboard/duplicateProject";
interface Project {
_id: string;
@@ -25,12 +27,25 @@ const DashboardProjects: React.FC = () => {
const [workspaceProjects, setWorkspaceProjects] = useState<WorkspaceProjects>(
{}
);
const [sharedwithMeProject, setSharedWithMeProjects] = useState<any>([])
const [projectDuplicateData, setProjectDuplicateData] = useState<Object>({});
const [isSearchActive, setIsSearchActive] = useState<boolean>(false);
const [activeFolder, setActiveFolder] = useState<string>("myProjects");
const { projectSocket } = useSocketStore();
const { userId, organization } = getUserData();
const handleProjectsSearch = async (inputValue: string) => {
if (!inputValue.trim()) {
setIsSearchActive(false);
return;
}
if (!setWorkspaceProjects || !setIsSearchActive) return;
const searchedProject = await searchProject(organization, userId, inputValue);
setIsSearchActive(true);
setWorkspaceProjects(searchedProject.message ? {} : searchedProject);
};
const fetchAllProjects = async () => {
try {
const projects = await getAllProjects(userId, organization);
@@ -44,18 +59,6 @@ const DashboardProjects: React.FC = () => {
}
};
const handleProjectsSearch = async (inputValue: string) => {
if (!inputValue.trim()) {
setIsSearchActive(false);
return;
}
if (!setWorkspaceProjects || !setIsSearchActive) return;
const searchedProject = await searchProject(organization, userId, inputValue);
setIsSearchActive(true);
setWorkspaceProjects(searchedProject.message ? {} : searchedProject);
};
const handleDeleteProject = async (projectId: any) => {
try {
// const deletedProject = await deleteProject(
@@ -97,18 +100,9 @@ const DashboardProjects: React.FC = () => {
const handleDuplicateWorkspaceProject = async (
projectId: string,
projectName: string,
thumbnail: string
thumbnail: string,
) => {
// await handleDuplicateProjects({
// userId,
// organization,
// projectId,
// projectName,
// projectSocket,
// thumbnail,
// setWorkspaceProjects,
// setIsSearchActive
// });
const duplicateProjectData = {
userId,
thumbnail,
@@ -121,9 +115,7 @@ const DashboardProjects: React.FC = () => {
const renderProjects = () => {
if (activeFolder !== "myProjects") return null;
const projectList = workspaceProjects[Object.keys(workspaceProjects)[0]];
if (!projectList?.length) {
return <div className="empty-state">No projects found</div>;
}
@@ -143,12 +135,49 @@ const DashboardProjects: React.FC = () => {
));
};
const renderSharedProjects = () => {
return sharedwithMeProject?.map((project: any) => (
<DashboardCard
key={project._id}
projectName={project.projectName}
thumbnail={project.thumbnail}
projectId={project._id}
createdAt={project.createdAt}
setIsSearchActive={setIsSearchActive}
active="shared"
createdBy={project.createdBy}
setProjectDuplicateData={setProjectDuplicateData}
handleDuplicateWorkspaceProject={handleDuplicateWorkspaceProject}
setActiveFolder={setActiveFolder}
/>
));
};
const sharedProject = async () => {
try {
const sharedWithMe = await sharedWithMeProjects();
console.log('sharedWithMe: ', sharedWithMe);
setSharedWithMeProjects(sharedWithMe)
} catch {
}
}
useEffect(() => {
if (!isSearchActive) {
fetchAllProjects();
}
}, [isSearchActive]);
useEffect(() => {
if (activeFolder === "shared") {
sharedProject()
}
}, [activeFolder])
return (
<div className="dashboard-home-container">
<DashboardNavBar
@@ -171,7 +200,8 @@ const DashboardProjects: React.FC = () => {
Shared with me
</button>
</div>
<div className="cards-container">{renderProjects()}</div>
<div className="cards-container">{activeFolder == "myProjects" ? renderProjects() : renderSharedProjects()}</div>
{projectDuplicateData && Object.keys(projectDuplicateData).length > 0 && (
<ProjectSocketRes
setIsSearchActive={setIsSearchActive}
@@ -183,4 +213,142 @@ const DashboardProjects: React.FC = () => {
);
};
export default DashboardProjects;
export default DashboardProjects;
// const MyProjects = () => {
// const { projectSocket } = useSocketStore();
// const { userId, organization } = getUserData();
// const fetchAllProjects = async () => {
// try {
// const projects = await getAllProjects(userId, organization);
// if (!projects || !projects.Projects) return;
// if (JSON.stringify(projects) !== JSON.stringify(workspaceProjects)) {
// setWorkspaceProjects(projects);
// }
// } catch (error) {
// console.error("Error fetching projects:", error);
// }
// };
// const handleDeleteProject = async (projectId: any) => {
// try {
// // const deletedProject = await deleteProject(
// // projectId,
// // userId,
// // organization
// // );
// // console.log('deletedProject: ', deletedProject);
// const deleteProjects = {
// projectId,
// organization,
// userId,
// };
// //socket for deleting the project
// if (projectSocket) {
// projectSocket.emit("v1:project:delete", deleteProjects);
// } else {
// console.error("Socket is not connected.");
// }
// setWorkspaceProjects((prevDiscardedProjects: WorkspaceProjects) => {
// if (!Array.isArray(prevDiscardedProjects?.Projects)) {
// return prevDiscardedProjects;
// }
// const updatedProjectDatas = prevDiscardedProjects.Projects.filter(
// (project) => project._id !== projectId
// );
// return {
// ...prevDiscardedProjects,
// Projects: updatedProjectDatas,
// };
// });
// setIsSearchActive(false);
// } catch (error) {
// console.error("Error deleting project:", error);
// }
// };
// const handleDuplicateWorkspaceProject = async (
// projectId: string,
// projectName: string,
// thumbnail: string
// ) => {
// // await handleDuplicateProjects({
// // userId,
// // organization,
// // projectId,
// // projectName,
// // projectSocket,
// // thumbnail,
// // setWorkspaceProjects,
// // setIsSearchActive
// // });
// const duplicateProjectData = {
// userId,
// thumbnail,
// organization,
// projectUuid: projectId,
// projectName,
// };
// projectSocket.emit("v1:project:Duplicate", duplicateProjectData);
// };
// const renderProjects = () => {
// if (activeFolder !== "myProjects") return null;
// const projectList = workspaceProjects[Object.keys(workspaceProjects)[0]];
// if (!projectList?.length) {
// return <div className="empty-state">No projects found</div>;
// }
// return projectList.map((project) => (
// <DashboardCard
// key={project._id}
// projectName={project.projectName}
// thumbnail={project.thumbnail}
// projectId={project._id}
// createdAt={project.createdAt}
// handleDeleteProject={handleDeleteProject}
// setIsSearchActive={setIsSearchActive}
// handleDuplicateWorkspaceProject={handleDuplicateWorkspaceProject}
// setProjectDuplicateData={setProjectDuplicateData}
// />
// ));
// };
// const renderSharedProjects = () => {
// if (activeFolder !== "shared") return null;
// const projectList = workspaceProjects[Object.keys(workspaceProjects)[0]];
// if (!projectList?.length) {
// return <div className="empty-state">No projects found</div>;
// }
// return projectList.map((project) => (
// <DashboardCard
// key={project._id}
// projectName={project.projectName}
// thumbnail={project.thumbnail}
// projectId={project._id}
// createdAt={project.createdAt}
// handleDeleteProject={handleDeleteProject}
// setIsSearchActive={setIsSearchActive}
// handleDuplicateWorkspaceProject={handleDuplicateWorkspaceProject}
// setProjectDuplicateData={setProjectDuplicateData}
// />
// ));
// };
// useEffect(() => {
// if (!isSearchActive) {
// fetchAllProjects();
// }
// }, [isSearchActive]);
// return null
// }

View File

@@ -14,12 +14,24 @@ interface Project {
createdAt: string;
isViewed?: string
}
interface RecentProject {
_id: string;
projectName: string;
thumbnail: string;
createdBy: { _id: string, userName: string };
projectUuid?: string;
createdAt: string;
isViewed?: string
}
interface RecentProjectData {
[key: string]: RecentProject[];
}
interface ProjectsData {
[key: string]: Project[];
}
interface ProjectSocketResProps {
setRecentProjects?: React.Dispatch<React.SetStateAction<ProjectsData>>;
setRecentProjects?: React.Dispatch<React.SetStateAction<RecentProjectData>>;
setWorkspaceProjects?: React.Dispatch<React.SetStateAction<ProjectsData>>;
setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
}
@@ -52,7 +64,7 @@ const ProjectSocketRes = ({
};
const handleDuplicate = async (data: any) => {
// console.log("Project duplicate response:", data);
console.log("Project duplicate response:", data);
if (data?.message === "Project Duplicated successfully") {
if (setWorkspaceProjects) {
const allProjects = await getAllProjects(userId, organization);

View File

@@ -5,6 +5,8 @@ import { RemoveIcon } from "../../../icons/ExportCommonIcons";
import PositionInput from "../customInput/PositionInputs";
import RotationInput from "../customInput/RotationInput";
import { useSelectedFloorItem, useObjectPosition, useObjectRotation } from "../../../../store/builder/store";
import { useSceneContext } from "../../../../modules/scene/sceneContext";
import { center } from "@turf/turf";
interface UserData {
id: number; // Unique identifier for the user data
@@ -18,6 +20,10 @@ const AssetProperties: React.FC = () => {
const { selectedFloorItem } = useSelectedFloorItem();
const { objectPosition } = useObjectPosition();
const { objectRotation } = useObjectRotation();
const { assetStore } = useSceneContext();
const { assets, setCurrentAnimation } = assetStore()
const [hoveredIndex, setHoveredIndex] = useState<any>(null);
const [isPlaying, setIsplaying] = useState(false);
// Function to handle adding new user data
const handleAddUserData = () => {
const newUserData: UserData = {
@@ -45,6 +51,12 @@ const AssetProperties: React.FC = () => {
);
};
const handleAnimationClick = (animation: string) => {
if (selectedFloorItem) {
const isPlaying = selectedFloorItem.animationState?.playing || false;
setCurrentAnimation(selectedFloorItem.uuid, animation, !isPlaying);
}
}
return (
<div className="asset-properties-container">
{/* Name */}
@@ -96,6 +108,40 @@ const AssetProperties: React.FC = () => {
+ Add
</div>
</section>
<div style={{ display: "flex", flexDirection: "column", outline: "1px solid var(--border-color)" }}>
{selectedFloorItem.uuid && <div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>Animations</div>}
{assets.map((asset) => (
<div key={asset.modelUuid} className="asset-item">
{asset.modelUuid === selectedFloorItem.uuid &&
asset.animations &&
asset.animations.length > 0 &&
asset.animations.map((animation, index) => (
<div
key={index}
style={{ gap: "15px", cursor: "pointer", padding: "5px" }}
>
<div
onClick={() => handleAnimationClick(animation)}
onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)}
style={{
height: "20px",
width: "100%",
borderRadius: "5px",
background:
hoveredIndex === index
? "#7b4cd3"
: "transparent",
}}
>
{animation.charAt(0).toUpperCase() +
animation.slice(1).toLowerCase()}
</div>
</div>
))}
</div>
))}
</div>
</div>
);
};

View File

@@ -133,11 +133,17 @@ const SelectedWallProperties = () => {
<InputWithDropDown
label="Height"
value={height}
min={1}
max={25}
step={1}
onChange={handleHeightChange}
/>
<InputWithDropDown
label="Thickness"
value={thickness}
min={0.1}
max={2}
step={0.1}
onChange={handleThicknessChange}
/>
</div>

View File

@@ -63,11 +63,17 @@ const WallProperties = () => {
<InputWithDropDown
label="Height"
value={`${wallHeight}`}
min={1}
max={25}
step={1}
onChange={(val) => handleHeightChange(val)}
/>
<InputWithDropDown
label="Thickness"
value={`${wallThickness}`}
min={0.1}
max={2}
step={0.1}
onChange={(val) => handleThicknessChange(val)}
/>
</div>

View File

@@ -7,15 +7,23 @@ import { access } from "fs";
import MultiEmailInvite from "../ui/inputs/MultiEmailInvite";
import { useActiveUsers } from "../../store/builder/store";
import { getUserData } from "../../functions/getUserData";
import { getProjectSharedList } from "../../services/factoryBuilder/collab/getProjectSharedList";
import { useParams } from "react-router-dom";
import { projection } from "@turf/turf";
import { shareAccess } from "../../services/factoryBuilder/collab/shareAccess";
import { getAvatarColor } from "../../modules/collaboration/functions/getAvatarColor";
interface UserListTemplateProps {
user: User;
}
const UserListTemplate: React.FC<UserListTemplateProps> = ({ user }) => {
const [accessSelection, setAccessSelection] = useState<string>(user.access);
const [accessSelection, setAccessSelection] = useState<string>(user?.Access);
const { projectId } = useParams();
function accessUpdate({ option }: AccessOption) {
const accessUpdate = async ({ option }: AccessOption) => {
if (!projectId) return
const accessSelection = await shareAccess(projectId, user.userId, option)
setAccessSelection(option);
}
@@ -26,18 +34,22 @@ const UserListTemplate: React.FC<UserListTemplateProps> = ({ user }) => {
{user.profileImage ? (
<img
src={user.profileImage || "https://via.placeholder.com/150"}
alt={`${user.name}'s profile`}
alt={`${user.
userName}'s profile`}
/>
) : (
<div
className="no-profile-container"
style={{ background: user.color }}
style={{ background: getAvatarColor(1, user.userId) }}
>
{user.name[0]}
{user.
userName.charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="user-name">{user.name}</div>
<div className="user-name"> {user.
userName.charAt(0).toUpperCase() + user.
userName.slice(1).toLowerCase()}</div>
</div>
<div className="user-access">
<RegularDropDown
@@ -61,39 +73,27 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
}) => {
const { activeUsers } = useActiveUsers();
const { userName } = getUserData();
const [users, setUsers] = useState([])
const { projectId } = useParams();
function getData() {
if (!projectId) return;
getProjectSharedList(projectId).then((allUser) => {
const accesMail = allUser?.datas || []
console.log('accesMail: ', accesMail);
setUsers(accesMail)
}).catch((err) => {
})
}
useEffect(() => {
// console.log("activeUsers: ", activeUsers);
getData();
}, [])
useEffect(() => {
//
}, [activeUsers]);
const users = [
{
name: "Alice Johnson",
email: "alice.johnson@example.com",
profileImage: "",
color: "#FF6600",
access: "Admin",
},
{
name: "Bob Smith",
email: "bob.smith@example.com",
profileImage: "",
color: "#488EF6",
access: "Viewer",
},
{
name: "Charlie Brown",
email: "charlie.brown@example.com",
profileImage: "",
color: "#48AC2A",
access: "Viewer",
},
{
name: "Diana Prince",
email: "diana.prince@example.com",
profileImage: "",
color: "#D44242",
access: "Viewer",
},
];
return (
<RenderOverlay>
<div
@@ -112,7 +112,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
<div className="header">
<div className="content">Share this file</div>
<div className="content">
<div className="copy-link-button">copy link</div>
{/* <div className="copy-link-button">copy link</div> */}
<div
className="close-button"
onClick={() => {
@@ -124,7 +124,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
</div>
</div>
<div className="invite-input-container">
<MultiEmailInvite />
<MultiEmailInvite users={users} getData={getData} />
</div>
<div className="split"></div>
<section>
@@ -142,7 +142,7 @@ const CollaborationPopup: React.FC<CollaborateProps> = ({
<div className="you-container">
<div className="your-name">
<div className="user-profile">{userName && userName[0].toUpperCase()}</div>
{userName}
{userName && userName.charAt(0).toUpperCase() + userName.slice(1).toLowerCase()}
</div>
<div className="indicater">you</div>
</div>

View File

@@ -6,6 +6,8 @@ import { useProjectName } from "../../store/builder/store";
import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { useComparisonProduct } from "../../store/simulation/useSimulationStore";
import { getUserData } from "../../functions/getUserData";
import { recentlyViewed } from "../../services/dashboard/recentlyViewed";
import { sharedWithMeProjects } from "../../services/dashboard/sharedWithMeProject";
interface LoadingPageProps {
progress: number; // Expect progress as a percentage (0-100)
@@ -19,18 +21,51 @@ const LoadingPage: React.FC<LoadingPageProps> = ({ progress }) => {
const validatedProgress = Math.min(100, Math.max(0, progress));
useEffect(() => {
if (!userId) return;
// useEffect(() => {
// if (!userId) return;
getAllProjects(userId, organization).then((projects) => {
if (!projects || !projects.Projects) return;
const filterProject = projects?.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId);
if (filterProject) {
setProjectName(filterProject.projectName);
// // getAllProjects(userId, organization).then((projects) => {
// // sharedWithMeProjects().then((shared) => {
// // console.log('filterProject: ', shared);
// // const filterProject = (projects?.Projects || shared)?.find((val: any) => val.projectUuid === projectId || val._id === projectId)
// // console.log('filterProject: ', filterProject);
// // // setProjectName(filterProject?.projectName)
// // })
// // }).catch(() => {
// // console.error("Error fetching projects")
// // })
// }, []);
useEffect(() => {
if (!userId) {
console.error("User data not found in localStorage");
return;
}
const fetchProjects = async () => {
try {
const projects = await getAllProjects(userId, organization);
const shared = await sharedWithMeProjects();
const allProjects = [...(projects?.Projects || []), ...(shared || [])];
const matchedProject = allProjects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
);
if (matchedProject) {
setProjectName(matchedProject.projectName);
} else {
console.warn("Project not found with given ID:", projectId);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
}).catch((error) => {
console.log(error);
})
};
fetchProjects();
}, []);
return (

View File

@@ -1,72 +1,135 @@
import React, { useState } from "react";
import { getSearchUsers } from "../../../services/factoryBuilder/collab/getSearchUsers";
import { useParams } from "react-router-dom";
import { shareProject } from "../../../services/factoryBuilder/collab/shareProject";
import { getUserData } from "../../../functions/getUserData";
const MultiEmailInvite: React.FC = () => {
const [emails, setEmails] = useState<string[]>([]);
interface UserData {
_id: string;
Email: string;
userName: string;
}
interface MultiEmailProps {
users: any,
getData: any,
}
const MultiEmailInvite: React.FC<MultiEmailProps> = ({ users, getData }) => {
const [emails, setEmails] = useState<any>([]);
const [searchedEmail, setSearchedEmail] = useState<UserData[]>([]);
const [inputFocus, setInputFocus] = useState(false);
const [inputValue, setInputValue] = useState("");
const { projectId } = useParams();
const { userId } = getUserData();
const handleAddEmail = () => {
const handleAddEmail = async (selectedUser: UserData) => {
if (!projectId || !selectedUser) return
const trimmedEmail = inputValue.trim();
setEmails((prev: any[]) => {
if (!selectedUser) return prev;
const isNotCurrentUser = selectedUser._id !== userId;
const alreadyExistsInEmails = prev.some(email => email._id === selectedUser._id);
const alreadyExistsInUsers = users.some((val: any) => val.userId === selectedUser._id);
if (isNotCurrentUser && !alreadyExistsInEmails && !alreadyExistsInUsers) {
return [...prev, selectedUser];
}
// Validate email
if (!trimmedEmail || !validateEmail(trimmedEmail)) {
alert("Please enter a valid email address.");
return;
}
// Check for duplicates
if (emails.includes(trimmedEmail)) {
alert("This email has already been added.");
return;
}
// Add email to the list
setEmails([...emails, trimmedEmail]);
return prev;
});
setInputValue(""); // Clear the input field after adding
};
const handleSearchMail = async (e: any) => {
setInputValue(e.target.value);
const trimmedEmail = e.target.value.trim();
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
handleAddEmail();
if (trimmedEmail.length < 3) return;
try {
const searchedMail = await getSearchUsers(trimmedEmail);
const filteredEmail = searchedMail.sharchMail?.filtered;
if (filteredEmail) {
setSearchedEmail(filteredEmail)
}
} catch (error) {
console.error("Failed to search mail:", error);
}
};
const handleRemoveEmail = (emailToRemove: string) => {
setEmails(emails.filter((email) => email !== emailToRemove));
const handleKeyDown = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === "," && searchedEmail.length > 0) {
e.preventDefault();
handleAddEmail(searchedEmail[0]);
}
};
const handleRemoveEmail = (idToRemove: string) => {
setEmails((prev: any) => prev.filter((email: any) => email._id !== idToRemove));
};
const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const [inputFocus, setInputFocus] = useState(false);
const handleInvite = () => {
if (!projectId) return;
try {
emails.forEach((user: any) => {
shareProject(user._id, projectId)
.then((res) => {
console.log("sharedProject:", res);
})
.catch((err) => {
console.error("Error sharing project:", err);
});
setEmails([])
setInputValue("")
});
setTimeout(() => {
getData()
}, 1000);
} catch (error) {
console.error("General error:", error);
}
};
return (
<div className="multi-email-invite-input-container">
<div className={`multi-email-invite-input${inputFocus ? " active" : ""}`}>
{emails.map((email, index) => (
{emails.map((email: any, index: number) => (
<div key={index} className="entered-emails">
{email}
<span onClick={() => handleRemoveEmail(email)}>&times;</span>
{email.Email}
<span onClick={() => handleRemoveEmail(email._id)}>&times;</span>
</div>
))}
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onChange={(e) => handleSearchMail(e)}
onFocus={() => setInputFocus(true)}
onBlur={() => setInputFocus(false)}
// onBlur={() => setInputFocus(false)}
onKeyDown={handleKeyDown}
placeholder="Enter email and press Enter or comma to seperate"
/>
</div>
<div onClick={handleAddEmail} className="invite-button">
Invite
</div>
<div className="users-list-container">
{/* list available users */}
<div onClick={handleInvite} className="invite-button">
Add
</div>
{inputFocus && inputValue.length > 2 && searchedEmail && searchedEmail.length > 0 && (
<div className="users-list-container">
{/* list available users here */}
{searchedEmail.map((val: any, i: any) => (
<div onClick={(e) => {
handleAddEmail(val)
setInputFocus(false)
}} key={i} >
{val?.Email}
</div>
))}
</div>
)}
</div>
);
};

View File

@@ -7,7 +7,7 @@ import { useBuilderStore } from '../../../store/builder/useBuilderStore';
import defaultMaterial from '../../../assets/textures/floor/wall-tex.png';
import useModuleStore from '../../../store/useModuleStore';
function DecalInstance({ visible = true, decal }: { visible?: boolean, decal: Decal }) {
function DecalInstance({ visible = true, decal, zPosition = decal.decalPosition[2] }: { visible?: boolean, decal: Decal, zPosition?: number }) {
const { setSelectedWall, setSelectedFloor, selectedDecal, setSelectedDecal } = useBuilderStore();
const { togglView } = useToggleView();
const { activeModule } = useModuleStore();
@@ -17,7 +17,7 @@ function DecalInstance({ visible = true, decal }: { visible?: boolean, decal: De
<Decal
// debug
visible={visible}
position={decal.decalPosition}
position={[decal.decalPosition[0], decal.decalPosition[1], zPosition]}
rotation={[0, 0, decal.decalRotation]}
scale={[decal.decalScale, decal.decalScale, 0.01]}
userData={decal}

View File

@@ -1,4 +1,4 @@
import * as THREE from "three"
import * as THREE from "three"
import { useEffect } from 'react'
import { getFloorAssets } from '../../../services/factoryBuilder/asset/floorAsset/getFloorItemsApi';
import { useLoadingProgress, useRenameModeStore, useSelectedFloorItem, useSelectedItem, useSocketStore } from '../../../store/builder/store';
@@ -269,7 +269,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
useEffect(() => {
const canvasElement = gl.domElement;
const onDrop = (event: any) => {
const onDrop = (event: DragEvent) => {
if (!event.dataTransfer?.files[0]) return;
if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category !== 'Fenestration') {

View File

@@ -6,7 +6,7 @@ import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import { ThreeEvent, useFrame, useThree } from '@react-three/fiber';
import { useActiveTool, useDeletableFloorItem, useLimitDistance, useRenderDistance, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store';
import { AssetBoundingBox } from '../../functions/assetBoundingBox';
import { CameraControls } from '@react-three/drei';
import { CameraControls, Html } from '@react-three/drei';
import useModuleStore, { useSubModuleStore } from '../../../../../store/useModuleStore';
import { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore';
import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore';
@@ -23,7 +23,7 @@ function Model({ asset }: { readonly asset: Asset }) {
const { subModule } = useSubModuleStore();
const { activeModule } = useModuleStore();
const { assetStore, eventStore, productStore } = useSceneContext();
const { removeAsset } = assetStore();
const { assets, removeAsset, setAnimations } = assetStore();
const { setTop } = useTopData();
const { setLeft } = useLeftData();
const { getIsEventInProduct } = productStore();
@@ -46,6 +46,9 @@ function Model({ asset }: { readonly asset: Asset }) {
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
const { userId, organization } = getUserData();
const [animationNames, setAnimationNames] = useState<string[]>([]);
const mixerRef = useRef<THREE.AnimationMixer>();
const actions = useRef<{ [name: string]: THREE.AnimationAction }>({});
useEffect(() => {
setDeletableFloorItem(null);
@@ -60,11 +63,45 @@ function Model({ asset }: { readonly asset: Asset }) {
const loadModel = async () => {
try {
// Check Cache
// const assetId = asset.assetId;
// const cachedModel = THREE.Cache.get(assetId);
// if (cachedModel) {
// setGltfScene(cachedModel.scene.clone());
// calculateBoundingBox(cachedModel.scene);
// return;
// }
// Check Cache
// const assetId = asset.assetId;
// console.log('assetId: ', assetId);
// const cachedModel = THREE.Cache.get(assetId);
// console.log('cachedModel: ', cachedModel);
// if (cachedModel) {
// setGltfScene(cachedModel.scene.clone());
// calculateBoundingBox(cachedModel.scene);
// return;
// }
const assetId = asset.assetId;
const cachedModel = THREE.Cache.get(assetId);
if (cachedModel) {
setGltfScene(cachedModel.scene.clone());
calculateBoundingBox(cachedModel.scene);
const clonedScene = cachedModel.scene.clone();
clonedScene.animations = cachedModel.animations || [];
setGltfScene(clonedScene);
calculateBoundingBox(clonedScene);
if (cachedModel.animations && clonedScene.animations.length > 0) {
const animationName = clonedScene.animations.map((clip: any) => clip.name);
setAnimationNames(animationName)
setAnimations(asset.modelUuid, animationName)
mixerRef.current = new THREE.AnimationMixer(clonedScene);
clonedScene.animations.forEach((animation: any) => {
const action = mixerRef.current!.clipAction(animation);
actions.current[animation.name] = action;
});
} else {
console.log('No animations');
}
return;
}
@@ -259,6 +296,32 @@ function Model({ asset }: { readonly asset: Asset }) {
clearSelectedAsset()
}
}
useFrame((_, delta) => {
if (mixerRef.current) {
mixerRef.current.update(delta);
}
});
useEffect(() => {
const handlePlay = (clipName: string) => {
console.log('clipName: ', clipName, asset.animationState);
if (!mixerRef.current) return;
Object.values(actions.current).forEach((action) => action.stop());
const action = actions.current[clipName];
if (action && asset.animationState?.playing) {
action.reset().setLoop(THREE.LoopOnce, 1).play();
console.log(`Playing: ${clipName}`);
} else {
console.warn(`No action found for: ${clipName}`);
}
};
handlePlay(asset.animationState?.current || '');
}, [asset])
return (
<group
@@ -306,7 +369,18 @@ function Model({ asset }: { readonly asset: Asset }) {
<AssetBoundingBox boundingBox={boundingBox} />
)
)}
</group>
{/* <group >
<Html>
<div style={{ position: 'absolute', }}>
{animationNames.map((name) => (
<button key={name} onClick={() => handlePlay(name)} style={{ margin: 4 }}>
{name}
</button>
))}
</div>
</Html>
</group> */}
</group >
);
}

View File

@@ -2,8 +2,8 @@
import * as THREE from "three";
import { useEffect, useRef } from "react";
import { useThree } from "@react-three/fiber";
import { Bvh } from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import { Geometry } from "@react-three/csg";
////////// Zustand State Imports //////////
@@ -39,16 +39,14 @@ import ZoneGroup from "./zone/zoneGroup";
import { useParams } from "react-router-dom";
import { useBuilderStore } from "../../store/builder/useBuilderStore";
import { getUserData } from "../../functions/getUserData";
import WallAssetGroup from "./wallAsset/wallAssetGroup";
export default function Builder() {
const state = useThree<Types.ThreeState>(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements.
const state = useThree<Types.ThreeState>();
const plane = useRef<THREE.Mesh>(null);
const csgRef = useRef<any>(null);
// Assigning the scene and camera from the Three.js state to the references.
const plane = useRef<THREE.Mesh>(null); // Reference for a plane object for raycaster reference.
const grid = useRef() as any; // Reference for a grid object for raycaster reference.
const { toggleView } = useToggleView(); // State for toggling between 2D and 3D.
const { toggleView } = useToggleView();
const { setToolMode } = useToolMode();
const { setRoofVisibility } = useRoofVisibility();
const { setWallVisibility } = useWallVisibility();
@@ -59,14 +57,6 @@ export default function Builder() {
const { setHoveredPoint, setHoveredLine } = useBuilderStore();
const { userId, organization } = getUserData();
// 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);
////////// All Toggle's //////////
useEffect(() => {
if (!toggleView) {
setHoveredLine(null);
@@ -91,29 +81,31 @@ export default function Builder() {
fetchVisibility();
}, []);
////////// Return //////////
useFrame(() => {
if (csgRef.current) {
csgRef.current.update();
}
})
return (
<>
<Ground grid={grid} plane={plane} />
<Ground plane={plane} />
<SocketResponses />
{/* <WallsAndWallItems
CSGGroup={CSGGroup}
setSelectedItemsIndex={setSelectedItemsIndex}
selectedItemsIndex={selectedItemsIndex}
currentWallItem={currentWallItem}
csg={csg}
lines={lines}
hoveredDeletableWallItem={hoveredDeletableWallItem}
/> */}
<AssetsGroup plane={plane} />
<AislesGroup />
<mesh name='Walls-And-WallAssets-Group'>
<Geometry ref={csgRef} useGroups>
<WallGroup />
<WallGroup />
<WallAssetGroup />
</Geometry>
</mesh>
<AislesGroup />
<FloorGroup />

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useToggleView } from '../../../store/builder/store'
import { useActiveTool, useToggleView } from '../../../store/builder/store'
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
import { useVersionContext } from '../version/versionContext';
import { useSceneContext } from '../../scene/sceneContext';
@@ -13,6 +13,7 @@ function FloorGroup() {
const { togglView } = useToggleView();
const { setSelectedFloor, setSelectedDecal } = useBuilderStore();
const { activeModule } = useModuleStore();
const { activeTool } = useActiveTool();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { floorStore } = useSceneContext();
@@ -24,7 +25,7 @@ function FloorGroup() {
setSelectedFloor(null);
setSelectedDecal(null);
}
}, [togglView, activeModule])
}, [togglView, activeModule, activeTool])
useEffect(() => {
if (projectId && selectedVersion) {

View File

@@ -0,0 +1,12 @@
import { Vector3 } from "three";
export default function closestPointOnLineSegment(p: Vector3, a: Vector3, b: Vector3) {
const ab = new Vector3().subVectors(b, a);
const ap = new Vector3().subVectors(p, a);
const abLengthSq = ab.lengthSq();
const dot = ap.dot(ab);
const t = Math.max(0, Math.min(1, dot / abLengthSq));
return new Vector3().copy(a).add(ab.multiplyScalar(t));
}

View File

@@ -154,7 +154,7 @@ function Wall({ wall }: { readonly wall: Wall }) {
<MeshDiscardMaterial />
{wall.decals.map((decal) => (
<DecalInstance visible={visible} key={decal.decalUuid} decal={decal} />
<DecalInstance zPosition={wall.wallThickness / 2 + 0.001} visible={visible} key={decal.decalUuid} decal={decal} />
))}
</mesh>
</mesh>

View File

@@ -1,6 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { DoubleSide, RepeatWrapping, Shape, SRGBColorSpace, TextureLoader, Vector2, Vector3 } from 'three';
import { Geometry } from '@react-three/csg';
import { Html, Extrude } from '@react-three/drei';
import { useLoader } from '@react-three/fiber';
import { useSceneContext } from '../../../scene/sceneContext';
@@ -44,13 +43,9 @@ function WallInstances() {
<>
{!toggleView && walls.length > 1 && (
<>
<mesh name='Walls-Group'>
<Geometry useGroups>
{walls.map((wall) => (
<WallInstance key={wall.wallUuid} wall={wall} />
))}
</Geometry>
</mesh>
{walls.map((wall) => (
<WallInstance key={wall.wallUuid} wall={wall} />
))}
<group name='Wall-Floors-Group'>
{rooms.map((room, index) => (

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useToggleView } from '../../../store/builder/store';
import { useActiveTool, useToggleView } from '../../../store/builder/store';
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
import { useVersionContext } from '../version/versionContext';
import { useSceneContext } from '../../scene/sceneContext';
@@ -14,6 +14,7 @@ function WallGroup() {
const { togglView } = useToggleView();
const { setSelectedWall, setSelectedDecal } = useBuilderStore();
const { activeModule } = useModuleStore();
const { activeTool } = useActiveTool();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { wallStore } = useSceneContext();
@@ -25,7 +26,7 @@ function WallGroup() {
setSelectedWall(null);
setSelectedDecal(null);
}
}, [togglView, activeModule])
}, [togglView, activeModule, activeTool])
useEffect(() => {
if (projectId && selectedVersion) {

View File

@@ -0,0 +1,313 @@
import * as THREE from 'three';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useThree } from '@react-three/fiber';
import { Base, Geometry, Subtraction } from '@react-three/csg';
import { retrieveGLTF, storeGLTF } from '../../../../../utils/indexDB/idbUtils';
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
import useModuleStore from '../../../../../store/useModuleStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useBuilderStore } from '../../../../../store/builder/useBuilderStore';
import { useActiveTool, useSocketStore, useToggleView } from '../../../../../store/builder/store';
import { useParams } from 'react-router-dom';
import { useVersionContext } from '../../../version/versionContext';
import { getUserData } from '../../../../../functions/getUserData';
import closestPointOnLineSegment from '../../../line/helpers/getClosestPointOnLineSegment';
import { upsertWallAssetApi } from '../../../../../services/factoryBuilder/asset/wallAsset/upsertWallAssetApi';
import { deleteWallAssetApi } from '../../../../../services/factoryBuilder/asset/wallAsset/deleteWallAssetApi';
function WallAssetInstance({ wallAsset }: { wallAsset: WallAsset }) {
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const { socket } = useSocketStore();
const { raycaster, pointer, camera, scene, controls, gl } = useThree();
const { wallStore, wallAssetStore } = useSceneContext();
const { walls, getWallById } = wallStore();
const { updateWallAsset, removeWallAsset } = wallAssetStore();
const { toggleView } = useToggleView();
const { activeTool } = useActiveTool();
const { activeModule } = useModuleStore();
const { selectedWallAsset, setSelectedWallAsset, setDeletableWallAsset, deletableWallAsset } = useBuilderStore();
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
const [boundingBox, setBoundingBox] = useState<THREE.Box3 | null>(null);
const groupRef = useRef<THREE.Group>(null);
const wall = useMemo(() => getWallById(wallAsset.wallUuid), [getWallById, wallAsset.wallUuid, walls]);
const draggingRef = useRef(false);
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
useEffect(() => {
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);
const loadModel = async () => {
try {
// Check Cache
const assetId = wallAsset.assetId;
const cachedModel = THREE.Cache.get(assetId);
if (cachedModel) {
setGltfScene(cachedModel.scene.clone());
calculateBoundingBox(cachedModel.scene);
return;
}
// Check IndexedDB
const indexedDBModel = await retrieveGLTF(assetId);
if (indexedDBModel) {
const blobUrl = URL.createObjectURL(indexedDBModel);
loader.load(blobUrl, (gltf) => {
URL.revokeObjectURL(blobUrl);
THREE.Cache.remove(blobUrl);
THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene);
},
undefined,
(error) => {
echo.error(`[IndexedDB] Error loading ${wallAsset.modelName}:`);
URL.revokeObjectURL(blobUrl);
}
);
return;
}
// Fetch from Backend
const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${assetId}`;
const handleBackendLoad = async (gltf: GLTF) => {
try {
const response = await fetch(modelUrl);
const modelBlob = await response.blob();
await storeGLTF(assetId, modelBlob);
THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene);
} catch (error) {
console.error(`[Backend] Error storing/loading ${wallAsset.modelName}:`, error);
}
};
loader.load(
modelUrl,
handleBackendLoad,
undefined,
(error) => {
echo.error(`[Backend] Error loading ${wallAsset.modelName}:`);
}
);
} catch (err) {
console.error("Failed to load model:", wallAsset.assetId, err);
}
};
const calculateBoundingBox = (scene: THREE.Object3D) => {
const box = new THREE.Box3().setFromObject(scene);
setBoundingBox(box);
};
loadModel();
}, []);
useEffect(() => {
const canvasElement = gl.domElement;
const onPointerUp = () => {
draggingRef.current = false;
if (controls) {
(controls as any).enabled = true;
}
};
const onPointerMove = (e: any) => {
if (!draggingRef.current || !wall || !selectedWallAsset) return;
if (controls) {
(controls as any).enabled = false;
}
pointer.x = (e.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
const intersect = intersects.find((i: any) => i.object.name.includes('WallReference'));
if (intersect && intersect.object.userData.wallUuid) {
const newPoint = closestPointOnLineSegment(
new THREE.Vector3(intersect.point.x, 0, intersect.point.z),
new THREE.Vector3(...intersect.object.userData.points[0].position),
new THREE.Vector3(...intersect.object.userData.points[1].position)
);
const wallRotation = intersect.object.rotation.clone();
const updatedWallAsset = updateWallAsset(wallAsset.modelUuid, {
wallUuid: intersect.object.userData.wallUuid,
position: [newPoint.x, wallAsset.wallAssetType === 'fixed-move' ? 0 : intersect.point.y, newPoint.z],
rotation: [wallRotation.x, wallRotation.y, wallRotation.z],
});
if (projectId && updatedWallAsset) {
// API
// upsertWallAssetApi(projectId, selectedVersion?.versionId || '', updatedWallAsset);
// SOCKET
const data = {
wallAssetData: updatedWallAsset,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:wall-asset:add', data);
}
}
};
if (selectedWallAsset && !toggleView && activeModule === 'builder') {
canvasElement.addEventListener('mousemove', onPointerMove);
canvasElement.addEventListener('pointerup', onPointerUp);
}
return () => {
canvasElement.removeEventListener('mousemove', onPointerMove);
canvasElement.removeEventListener('pointerup', onPointerUp);
};
}, [gl, camera, toggleView, activeModule, selectedWallAsset, socket])
const handlePointerClick = useCallback((wallAsset: WallAsset) => {
if (activeTool === 'delete' && deletableWallAsset && deletableWallAsset.userData.modelUuid === wallAsset.modelUuid) {
const removedWallAsset = removeWallAsset(wallAsset.modelUuid);
if (projectId && removedWallAsset) {
// API
// deleteWallAssetApi(projectId, selectedVersion?.versionId || '', removedWallAsset.modelUuid, removedWallAsset.wallUuid);
// SOCKET
const data = {
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization,
modelUuid: removedWallAsset.modelUuid,
wallUuid: removedWallAsset.wallUuid
}
socket.emit('v1:wall-asset:delete', data);
}
}
}, [activeTool, activeModule, deletableWallAsset, socket])
const handlePointerOver = useCallback((wallAsset: WallAsset, currentObject: THREE.Object3D) => {
if (activeTool === "delete" && activeModule === 'builder') {
if (deletableWallAsset && deletableWallAsset.userData.modelUuid === wallAsset.modelUuid) {
return;
} else {
setDeletableWallAsset(currentObject);
}
}
}, [activeTool, activeModule, deletableWallAsset]);
const handlePointerOut = useCallback((wallAsset: WallAsset) => {
if (activeTool === "delete" && deletableWallAsset && deletableWallAsset.userData.modelUuid === wallAsset.modelUuid) {
setDeletableWallAsset(null);
}
}, [activeTool, deletableWallAsset]);
if (!gltfScene || !boundingBox || !wall) { return null }
const size = new THREE.Vector3();
boundingBox.getSize(size);
const center = new THREE.Vector3();
boundingBox.getCenter(center);
return (
<>
<group
key={wallAsset.modelUuid}
name={wallAsset.modelName}
ref={groupRef}
position={wallAsset.position}
rotation={wallAsset.rotation}
visible={wallAsset.isVisible}
userData={wallAsset}
>
<Subtraction position={[center.x, center.y, center.z]} scale={[size.x, size.y, wall.wallThickness + 0.05]}>
<Geometry>
<Base geometry={new THREE.BoxGeometry()} />
</Geometry>
</Subtraction>
{gltfScene && (
<mesh
onPointerDown={(e) => {
if (!toggleView && activeModule === 'builder' && selectedWallAsset && selectedWallAsset.userData.modelUuid === wallAsset.modelUuid) {
draggingRef.current = true;
e.stopPropagation();
setSelectedWallAsset(groupRef.current);
if (controls) {
(controls as any).enabled = false;
}
}
}}
onClick={(e) => {
if (!toggleView && activeModule === 'builder' && activeTool !== 'delete') {
if (e.object) {
e.stopPropagation();
let currentObject = e.object as THREE.Object3D;
while (currentObject) {
if (currentObject.name === "Scene") {
break;
}
currentObject = currentObject.parent as THREE.Object3D;
}
setSelectedWallAsset(currentObject);
}
} else if (!toggleView && activeModule === 'builder' && activeTool === 'delete') {
handlePointerClick(wallAsset);
}
}}
onPointerMissed={() => {
if (selectedWallAsset && selectedWallAsset.userData.modelUuid === wallAsset.modelUuid) {
setSelectedWallAsset(null);
}
}}
onPointerEnter={(e) => {
if (!toggleView) {
e.stopPropagation();
let currentObject = e.object as THREE.Object3D;
while (currentObject) {
if (currentObject.name === "Scene") {
break;
}
currentObject = currentObject.parent as THREE.Object3D;
}
handlePointerOver(wallAsset, currentObject);
}
}}
onPointerOut={(e) => {
if (!toggleView) {
e.stopPropagation();
handlePointerOut(wallAsset);
}
}}
userData={wallAsset}
>
<primitive userData={wallAsset} object={gltfScene} />
</mesh>
)}
</group>
</>
)
}
export default WallAssetInstance

View File

@@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { useSceneContext } from '../../../scene/sceneContext'
import { useToggleView } from '../../../../store/builder/store';
import WallAssetInstance from './Instances/wallAssetInstance';
function WallAssetInstances() {
const { wallAssetStore } = useSceneContext();
const { wallAssets } = wallAssetStore();
const { toggleView } = useToggleView();
useEffect(() => {
// console.log('wallAssets: ', wallAssets);
}, [wallAssets])
return (
<>
{!toggleView && wallAssets.length > 0 && (
<>
{wallAssets.map((wallAsset) => (
<WallAssetInstance key={wallAsset.modelUuid} wallAsset={wallAsset} />
))}
</>
)}
</>
)
}
export default WallAssetInstances

View File

@@ -0,0 +1,103 @@
import { useThree } from '@react-three/fiber';
import { useEffect } from 'react'
import useModuleStore from '../../../store/useModuleStore';
import { useSelectedItem, useSocketStore, useToggleView } from '../../../store/builder/store';
import { MathUtils, Vector3 } from 'three';
import { useSceneContext } from '../../scene/sceneContext';
import { useParams } from 'react-router-dom';
import { useVersionContext } from '../version/versionContext';
import { getUserData } from '../../../functions/getUserData';
import closestPointOnLineSegment from '../line/helpers/getClosestPointOnLineSegment';
import { upsertWallAssetApi } from '../../../services/factoryBuilder/asset/wallAsset/upsertWallAssetApi';
function WallAssetCreator() {
const { socket } = useSocketStore();
const { pointer, camera, raycaster, scene, gl } = useThree();
const { togglView } = useToggleView();
const { activeModule } = useModuleStore();
const { wallAssetStore } = useSceneContext();
const { addWallAsset } = wallAssetStore();
const { selectedItem, setSelectedItem } = useSelectedItem();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
useEffect(() => {
const canvasElement = gl.domElement;
const onDrop = (event: DragEvent) => {
if (!event.dataTransfer?.files[0]) return;
if (selectedItem.id !== "" && event.dataTransfer?.files[0] && selectedItem.category === 'Fenestration') {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
const intersect = intersects.find((intersect) => intersect.object.name.includes('WallReference'));
if (intersect) {
const wall = intersect.object.userData as Wall;
const closestPoint = closestPointOnLineSegment(
new Vector3(intersect.point.x, 0, intersect.point.z),
new Vector3(...wall.points[0].position),
new Vector3(...wall.points[1].position)
)
const wallRotation = intersect.object.rotation.clone();
const newWallAsset: WallAsset = {
modelName: selectedItem.name,
modelUuid: MathUtils.generateUUID(),
wallUuid: wall.wallUuid,
wallAssetType: selectedItem.subCategory,
assetId: selectedItem.id,
position: [closestPoint.x, selectedItem.subCategory === "fixed-move" ? 0 : intersect.point.y, closestPoint.z],
rotation: [wallRotation.x, wallRotation.y, wallRotation.z],
isLocked: false,
isVisible: true,
opacity: 1,
};
addWallAsset(newWallAsset);
if (projectId) {
// API
// upsertWallAssetApi(projectId, selectedVersion?.versionId || '', newWallAsset);
// SOCKET
const data = {
wallAssetData: newWallAsset,
projectId: projectId,
versionId: selectedVersion?.versionId || '',
userId: userId,
organization: organization
}
socket.emit('v1:wall-asset:add', data);
}
}
}
};
if (!togglView && activeModule === 'builder') {
canvasElement.addEventListener('drop', onDrop);
}
return () => {
canvasElement.removeEventListener('drop', onDrop);
};
}, [gl, camera, togglView, activeModule, socket, selectedItem, setSelectedItem]);
return (
<>
</>
)
}
export default WallAssetCreator

View File

@@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { useActiveTool, useToggleView } from '../../../store/builder/store';
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
import { useVersionContext } from '../version/versionContext';
import { useSceneContext } from '../../scene/sceneContext';
import { useParams } from 'react-router-dom';
import useModuleStore from '../../../store/useModuleStore';
import WallAssetCreator from './wallAssetCreator'
import WallAssetInstances from './Instances/wallAssetInstances'
import { getWallAssetsApi } from '../../../services/factoryBuilder/asset/wallAsset/getWallAssetsApi';
function WallAssetGroup() {
const { togglView } = useToggleView();
const { setSelectedFloorAsset, setDeletableWallAsset } = useBuilderStore();
const { activeModule } = useModuleStore();
const { activeTool } = useActiveTool();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { wallAssetStore } = useSceneContext();
const { setWallAssets } = wallAssetStore();
const { projectId } = useParams();
useEffect(() => {
if (togglView || activeModule !== 'builder') {
setSelectedFloorAsset(null);
}
setDeletableWallAsset(null);
}, [togglView, activeModule, activeTool])
useEffect(() => {
if (projectId && selectedVersion) {
getWallAssetsApi(projectId, selectedVersion?.versionId || '').then((wallAssets) => {
console.log('wallAssets: ', wallAssets);
if (wallAssets && wallAssets.length > 0) {
setWallAssets(wallAssets);
} else {
setWallAssets([]);
}
}).catch((err) => {
console.log(err);
})
}
}, [projectId, selectedVersion?.versionId])
return (
<>
<WallAssetCreator />
<WallAssetInstances />
</>
)
}
export default WallAssetGroup

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { useToggleView } from '../../../store/builder/store';
import { useActiveTool, useToggleView } from '../../../store/builder/store';
import { useBuilderStore } from '../../../store/builder/useBuilderStore';
import { useVersionContext } from '../version/versionContext';
import { useSceneContext } from '../../scene/sceneContext';
@@ -14,6 +14,7 @@ function ZoneGroup() {
const { togglView } = useToggleView();
const { setSelectedZone } = useBuilderStore();
const { activeModule } = useModuleStore();
const { activeTool } = useActiveTool();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { zoneStore } = useSceneContext();
@@ -24,12 +25,11 @@ function ZoneGroup() {
if (togglView || activeModule !== 'builder') {
setSelectedZone(null);
}
}, [togglView, activeModule])
}, [togglView, activeModule, activeTool])
useEffect(() => {
if (projectId && selectedVersion) {
getZonesApi(projectId, selectedVersion?.versionId || '').then((zones) => {
console.log('zones: ', zones);
if (zones && zones.length > 0) {
setZones(zones);
} else {

View File

@@ -1,6 +1,17 @@
import React from 'react';
import { useEffect } from 'react';
import { useSocketStore } from '../../../store/builder/store';
export default function SocketResponses() {
const { socket } = useSocketStore();
useEffect(() => {
socket.on("v1:wall-asset:response:delete", (data: any) => {
});
return () => {
socket.off("v1:wall-asset:response:delete");
}
}, [socket])
return (
<>

View File

@@ -225,7 +225,7 @@ const SelectionControls: React.FC = () => {
selectedObjects.forEach((object) => {
let currentObject: THREE.Object3D | null = object;
while (currentObject) {
if (currentObject.userData.modelUuid) {
if (currentObject.userData.modelUuid && !currentObject.userData.wallAssetType) {
Objects.add(currentObject);
break;
}

View File

@@ -1,14 +1,13 @@
import { useTileDistance, useToggleView } from "../../../store/builder/store";
import * as CONSTANTS from "../../../types/world/worldConstants";
const Ground = ({ grid, plane }: any) => {
const Ground = ({ plane }: any) => {
const { toggleView } = useToggleView();
const { planeValue, gridValue } = useTileDistance();
return (
<mesh name="Ground">
<mesh
ref={grid}
name="Grid"
position={!toggleView ? CONSTANTS.gridConfig.position3D : CONSTANTS.gridConfig.position2D}
>

View File

@@ -15,7 +15,7 @@ export default function PostProcessing() {
const { selectedWallItem } = useSelectedWallItem();
const { selectedFloorItem } = useSelectedFloorItem();
const { selectedEventSphere } = useSelectedEventSphere();
const { selectedAisle, selectedWall, selectedDecal, selectedFloor } = useBuilderStore();
const { selectedAisle, selectedWall, selectedDecal, selectedFloor, selectedWallAsset, deletableWallAsset } = useBuilderStore();
function flattenChildren(children: any[]) {
const allChildren: any[] = [];
@@ -48,6 +48,14 @@ export default function PostProcessing() {
// console.log('selectedFloor: ', selectedFloor);
}, [selectedFloor])
useEffect(() => {
// console.log('selectedWallAsset: ', selectedWallAsset);
}, [selectedWallAsset])
useEffect(() => {
// console.log('deletableWallAsset: ', deletableWallAsset);
}, [deletableWallAsset])
return (
<EffectComposer autoClear={false}>
<N8AO
@@ -59,6 +67,36 @@ export default function PostProcessing() {
denoiseRadius={6}
denoiseSamples={16}
/>
{selectedWallAsset && (
<Outline
selection={flattenChildren(selectedWallAsset.children)}
selectionLayer={10}
width={2000}
blendFunction={BlendFunction.ALPHA}
edgeStrength={5}
resolutionScale={2}
pulseSpeed={0}
visibleEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
hiddenEdgeColor={CONSTANTS.outlineConfig.assetSelectColor}
blur={true}
xRay={true}
/>
)}
{deletableWallAsset && (
<Outline
selection={flattenChildren(deletableWallAsset.children)}
selectionLayer={10}
width={2000}
blendFunction={BlendFunction.ALPHA}
edgeStrength={5}
resolutionScale={2}
pulseSpeed={0}
visibleEdgeColor={CONSTANTS.outlineConfig.assetDeleteColor}
hiddenEdgeColor={CONSTANTS.outlineConfig.assetDeleteColor}
blur={true}
xRay={true}
/>
)}
{selectedAisle && (
<Outline
selection={flattenChildren(selectedAisle.children)}

View File

@@ -40,10 +40,10 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
if (!canvas) return;
const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png");
const updateProjects = {
projectId: project.projectUuid,
projectId: project?.projectUuid,
organization,
userId,
projectName: project.projectName,
projectName: project?.projectName,
thumbnail: screenshotDataUrl,
};
if (projectSocket) {

View File

@@ -29,6 +29,8 @@ import { SceneProvider } from "../modules/scene/sceneContext";
import { getVersionHistoryApi } from "../services/factoryBuilder/versionControl/getVersionHistoryApi";
import { useVersionHistoryStore } from "../store/builder/useVersionHistoryStore";
import { VersionProvider } from "../modules/builder/version/versionContext";
import { recentlyViewed } from "../services/dashboard/recentlyViewed";
import { sharedWithMeProjects } from "../services/dashboard/sharedWithMeProject";
const Project: React.FC = () => {
let navigate = useNavigate();
@@ -54,18 +56,32 @@ const Project: React.FC = () => {
return;
}
getAllProjects(userId, organization).then((projects) => {
if (!projects || !projects.Projects) return;
const filterProject = projects?.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId)
setProjectName(filterProject.projectName)
viewProject(organization, filterProject._id, userId).then((viewedProject) => {
});
}).catch(() => {
console.error("Error fetching projects")
})
const fetchProjects = async () => {
try {
const projects = await getAllProjects(userId, organization);
const shared = await sharedWithMeProjects();
const allProjects = [...(projects?.Projects || []), ...(shared || [])];
const matchedProject = allProjects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
);
if (matchedProject) {
setProjectName(matchedProject.projectName);
await viewProject(organization, matchedProject._id, userId);
} else {
console.warn("Project not found with given ID:", projectId);
}
} catch (error) {
console.error("Error fetching projects:", error);
}
};
fetchProjects();
}, []);
useEffect(() => {
if (!projectId) return;
getVersionHistoryApi(projectId).then((data) => {

View File

@@ -0,0 +1,30 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const sharedWithMeProjects = async (
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/sharedWithMe`, {
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
});
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
//console.log("New token received:", newAccessToken);
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
throw new Error("Failed to fetch assets");
}
return await response.json();
} catch (error: any) {
echo.error("Failed to get asset image");
console.log(error.message);
}
};

View File

@@ -0,0 +1,40 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const deleteWallAssetApi = async (
projectId: string,
versionId: string,
modelUuid: string,
wallUuid: string,
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/delete/wallasset`, {
method: "PATCH",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ projectId, versionId, modelUuid, wallUuid }),
});
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
console.error("Failed to delete wall asset:", response.statusText);
}
const result = await response.json();
return result;
} catch (error) {
echo.error("Failed to delete wall asset");
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
}
}
};

View File

@@ -0,0 +1,37 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const getWallAssetsApi = async (
projectId: string,
versionId: string,
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/wallassets/${projectId}/${versionId}`, {
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
});
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
console.error("Failed to get wall assets:", response.statusText);
}
const result = await response.json();
return result;
} catch (error) {
echo.error("Failed to get wall assets");
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
}
}
};

View File

@@ -0,0 +1,39 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const upsertWallAssetApi = async (
projectId: string,
versionId: string,
wallAssetData: WallAsset
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/wall-asset`, {
method: "POST",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ projectId, versionId, wallAssetData }),
});
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
localStorage.setItem("token", newAccessToken);
}
if (!response.ok) {
console.error("Failed to upsert wall asset:", response.statusText);
}
const result = await response.json();
return result;
} catch (error) {
echo.error("Failed to upsert wall asset");
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
}
}
};

View File

@@ -0,0 +1,27 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const getProjectSharedList = async (projectId: string) => {
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/V1/projectsharedList?projectId=${projectId}`,
{
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (!response.ok) {
throw new Error("Failed to get users");
}
return await response.json();
} catch (error: any) {
echo.error("Failed to get users");
console.log(error.message);
}
};

View File

@@ -0,0 +1,27 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const getSearchUsers = async (searchMail: string) => {
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/V1/searchMail?searchMail=${searchMail}`,
{
method: "GET",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
}
);
if (!response.ok) {
throw new Error("Failed to get users");
}
return await response.json();
} catch (error: any) {
echo.error("Failed to get users");
console.log(error.message);
}
};

View File

@@ -0,0 +1,43 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
// let url_Backend_dwinzo = `http://192.168.0.102:5000`;
export const shareAccess = async (
projectId: string,
targetUserId: string,
newAccessPoint: string
) => {
const body: any = {
projectId,
targetUserId,
newAccessPoint
};
try {
const response = await fetch(
`${url_Backend_dwinzo}/api/V1/sharedAccespoint`,
{
method: "PATCH",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify(body),
}
);
if (!response.ok) {
console.error("Failed to clearPanel in the zone");
}
const result = await response.json();
return result;
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error("An unknown error occurred");
}
}
};

View File

@@ -0,0 +1,30 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const shareProject = async (addUserId: string, projectId: string) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/projectshared`, {
method: "POST",
headers: {
Authorization: "Bearer <access_token>",
"Content-Type": "application/json",
token: localStorage.getItem("token") || "",
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ addUserId, projectId }),
});
if (!response.ok) {
console.error("Failed to add project");
}
const result = await response.json();
console.log("result: ", result);
return result;
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("An unknown error occurred");
}
}
};

View File

@@ -6,7 +6,7 @@ export const upsertZoneApi = async (
zoneData: Zone
) => {
try {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/UpsertZone`, {
const response = await fetch(`${url_Backend_dwinzo}/api/V1/upsertZone`, {
method: "POST",
headers: {
Authorization: "Bearer <access_token>",

View File

@@ -21,7 +21,7 @@ interface AssetsStore {
setOpacity: (modelUuid: string, opacity: number) => void;
// Animation controls
setAnimation: (modelUuid: string, animation: string) => void;
setAnimations: (modelUuid: string, animations: string[]) => void;
setCurrentAnimation: (modelUuid: string, current: string, isPlaying: boolean) => void;
addAnimation: (modelUuid: string, animation: string) => void;
removeAnimation: (modelUuid: string, animation: string) => void;
@@ -143,14 +143,13 @@ export const createAssetStore = () => {
},
// Animation controls
setAnimation: (modelUuid, animation) => {
setAnimations: (modelUuid, animations) => {
set((state) => {
const asset = state.assets.find(a => a.modelUuid === modelUuid);
if (asset) {
asset.animations = animations;
if (!asset.animationState) {
asset.animationState = { current: animation, playing: false };
} else {
asset.animationState.current = animation;
asset.animationState = { current: '', playing: false };
}
}
});

View File

@@ -9,6 +9,13 @@ interface BuilderState {
snappedPosition: [number, number, number] | null;
hoveredLine: [Point, Point] | null;
// Wall Asset
selectedWallAsset: Object3D | null;
deletableWallAsset: Object3D | null;
// Floor Asset
selectedFloorAsset: Object3D | null;
// Wall Settings
selectedWall: Object3D | null;
wallThickness: number;
@@ -51,6 +58,13 @@ interface BuilderState {
setSnappedPosition: (position: [number, number, number] | null) => void;
setHoveredLine: (line: [Point, Point] | null) => void;
// Setters - Wall Asset
setSelectedWallAsset: (asset: Object3D | null) => void;
setDeletableWallAsset: (asset: Object3D | null) => void;
// Setters - Floor Asset
setSelectedFloorAsset: (asset: Object3D | null) => void;
// Setters - Wall
setSelectedWall: (wall: Object3D | null) => void;
setWallThickness: (thickness: number) => void;
@@ -100,6 +114,11 @@ export const useBuilderStore = create<BuilderState>()(
snappedPosition: null,
hoveredLine: null,
selectedWallAsset: null,
deletableWallAsset: null,
selectedFloorAsset: null,
selectedWall: null,
wallThickness: 0.5,
wallHeight: 7,
@@ -156,6 +175,28 @@ export const useBuilderStore = create<BuilderState>()(
})
},
// === Setters: Wall Asset ===
setSelectedWallAsset(asset: Object3D | null) {
set((state) => {
state.selectedWallAsset = asset;
});
},
setDeletableWallAsset(asset: Object3D | null) {
set((state) => {
state.deletableWallAsset = asset;
});
},
// === Setters: Floor Asset ===
setSelectedFloorAsset(asset: Object3D | null) {
set((state) => {
state.selectedFloorAsset = asset;
});
},
// === Setters: Wall ===
setSelectedWall: (wall: Object3D | null) => {

View File

@@ -5,8 +5,9 @@ interface WallAssetStore {
wallAssets: WallAsset[];
setWallAssets: (assets: WallAsset[]) => void;
addWallAsset: (asset: WallAsset) => void;
updateWallAsset: (uuid: string, updated: Partial<WallAsset>) => void;
removeWallAsset: (uuid: string) => void;
updateWallAsset: (uuid: string, updated: Partial<WallAsset>) => WallAsset | undefined;
setWallAssetPosition: (uuid: string, position: [number, number, number]) => void;
removeWallAsset: (uuid: string) => WallAsset | undefined;
clearWallAssets: () => void;
setVisibility: (uuid: string, isVisible: boolean) => void;
@@ -30,16 +31,36 @@ export const createWallAssetStore = () => {
state.wallAssets.push(asset);
}),
updateWallAsset: (uuid, updated) => set(state => {
updateWallAsset: (uuid, updated) => {
let updatedWallAsset: WallAsset | undefined;
set(state => {
const asset = state.wallAssets.find(a => a.modelUuid === uuid);
if (asset) {
Object.assign(asset, updated);
updatedWallAsset = JSON.parse(JSON.stringify(asset));
}
});
return updatedWallAsset;
},
setWallAssetPosition: (uuid, position) => set(state => {
const asset = state.wallAssets.find(a => a.modelUuid === uuid);
if (asset) {
Object.assign(asset, updated);
asset.position = position;
}
}),
removeWallAsset: (uuid) => set(state => {
state.wallAssets = state.wallAssets.filter(a => a.modelUuid !== uuid);
}),
removeWallAsset: (uuid) => {
let removedAsset: WallAsset | undefined;
set(state => {
const asset = state.wallAssets.find(a => a.modelUuid === uuid);
if (asset) {
removedAsset = JSON.parse(JSON.stringify(asset));
state.wallAssets = state.wallAssets.filter(a => a.modelUuid !== uuid);
}
});
return removedAsset;
},
clearWallAssets: () => {
set(state => {

View File

@@ -46,7 +46,6 @@ textarea {
}
input[type="number"] {
// Chrome, Safari, Edge, Opera
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
@@ -487,7 +486,6 @@ input[type="number"] {
position: relative;
cursor: pointer;
.check-box-style {
position: absolute;
height: 20px;
@@ -665,6 +663,21 @@ input[type="number"] {
.multi-email-invite-input-container {
@include flex-space-between;
gap: 20px;
position: relative;
.users-list-container {
position: absolute;
top: calc(100% + 8px);
background: var(--background-color);
backdrop-filter: blur(18px);
padding: 12px;
width: 100%;
height: auto;
max-height: 200px;
border-radius: 8px;
z-index: 100;
outline: 1px solid var(--border-color);
}
.multi-email-invite-input {
width: 100%;
@@ -764,4 +777,4 @@ input[type="number"] {
background: var(--background-color-gray);
}
}
}
}

View File

@@ -51,9 +51,11 @@ type Assets = Asset[];
interface WallAsset {
modelUuid: string;
modelName: string;
wallAssetType: string;
assetId: string;
wallUuid: string;
position: [number, number, number];
rotation: [number, number, number];
isLocked: boolean;
isVisible: boolean;
opacity: number;

View File

@@ -1,13 +1,22 @@
export interface User {
name: string;
email: string;
profileImage: string;
color: string;
access: string;
userName: string;
Email: string;
Access: string;
userId: string;
profileImage?: string;
color?: string;
}
// export interface User {
// name: string;
// email: string;
// profileImage: string;
// color: string;
// access: string;
// }
type AccessOption = {
option: string;
option: string;
};
export type ActiveUser = {
@@ -15,7 +24,7 @@ export type ActiveUser = {
userName: string;
email: string;
activeStatus?: string; // Optional property
position?: { x: number; y: number; z: number; };
rotation?: { x: number; y: number; z: number; };
target?: { x: number; y: number; z: number; };
};
position?: { x: number; y: number; z: number };
rotation?: { x: number; y: number; z: number };
target?: { x: number; y: number; z: number };
};