Merge branch 'main-demo' into dev-physics

This commit is contained in:
2025-08-21 18:14:26 +05:30
116 changed files with 7262 additions and 2456 deletions

View File

@@ -2,289 +2,314 @@ 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";
import { useLoadingProgress, useProjectName, useSocketStore } from "../../store/builder/store";
import {
useLoadingProgress,
useProjectName,
useSocketStore,
} from "../../store/builder/store";
import { viewProject } from "../../services/dashboard/viewProject";
import OuterClick from "../../utils/outerClick";
import { KebabIcon } from "../icons/ExportCommonIcons";
import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { updateProject } from "../../services/dashboard/updateProject";
interface DashBoardCardProps {
projectName: string;
thumbnail: any;
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,
userId?: string
) => Promise<void>;
handleDuplicateRecentProject?: (
projectId: string,
projectName: string,
thumbnail: string
) => Promise<void>;
active?: string;
setIsSearchActive?: React.Dispatch<React.SetStateAction<boolean>>;
setRecentDuplicateData?: React.Dispatch<React.SetStateAction<Object>>;
setProjectDuplicateData?: React.Dispatch<React.SetStateAction<Object>>;
setActiveFolder?: React.Dispatch<React.SetStateAction<string>>;
projectName: string;
thumbnail: any;
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,
userId?: string
) => Promise<void>;
handleDuplicateRecentProject?: (
projectId: string,
projectName: string,
thumbnail: string
) => Promise<void>;
active?: string;
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;
const DashboardCard: React.FC<DashBoardCardProps> = ({
projectName,
thumbnail,
projectId,
active,
handleDeleteProject,
handleRestoreProject,
handleTrashDeleteProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
createdAt,
createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder
projectName,
thumbnail,
projectId,
active,
handleDeleteProject,
handleRestoreProject,
handleTrashDeleteProject,
handleDuplicateWorkspaceProject,
handleDuplicateRecentProject,
createdAt,
createdBy,
setRecentDuplicateData,
setProjectDuplicateData,
setActiveFolder,
}) => {
const navigate = useNavigate();
const { setProjectName } = useProjectName();
const { userId, organization, userName } = getUserData();
const [isKebabOpen, setIsKebabOpen] = useState(false);
const [renameValue, setRenameValue] = useState(projectName);
const [isRenaming, setIsRenaming] = useState(false);
const { projectSocket } = useSocketStore();
const { setLoadingProgress } = useLoadingProgress();
const kebabRef = useRef<HTMLDivElement>(null);
const navigate = useNavigate();
const { setProjectName } = useProjectName();
const { userId, organization, userName } = getUserData();
const [isKebabOpen, setIsKebabOpen] = useState(false);
const [renameValue, setRenameValue] = useState(projectName);
const [isRenaming, setIsRenaming] = useState(false);
const { projectSocket } = useSocketStore();
const { setLoadingProgress } = useLoadingProgress();
const kebabRef = useRef<HTMLDivElement>(null);
const navigateToProject = async (e: any) => {
console.log('active: ', active);
if (active && active == "trash") return;
try {
const viewProjects = await viewProject(organization, projectId, userId)
console.log('viewProjects: ', viewProjects);
console.log('projectName: ', projectName);
setLoadingProgress(1)
setProjectName(projectName);
navigate(`/${projectId}`);
} catch {
const navigateToProject = async (e: any) => {
if (active && active == "trash") return;
try {
const viewProjects = await viewProject(organization, projectId, userId);
setLoadingProgress(1);
setProjectName(projectName);
navigate(`/${projectId}`);
} catch {}
};
const handleOptionClick = async (option: string) => {
switch (option) {
case "delete":
if (handleDeleteProject) {
handleDeleteProject(projectId);
} else if (handleTrashDeleteProject) {
handleTrashDeleteProject(projectId);
}
};
const handleOptionClick = async (option: string) => {
switch (option) {
case "delete":
if (handleDeleteProject) {
handleDeleteProject(projectId);
} else if (handleTrashDeleteProject) {
handleTrashDeleteProject(projectId);
}
break;
case "restore":
if (handleRestoreProject) {
await handleRestoreProject(projectId);
}
break;
case "open in new tab":
try {
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) {
}
window.open(`/${projectId}`, "_blank");
break;
case "rename":
setIsRenaming(true);
break;
case "duplicate":
if (handleDuplicateWorkspaceProject) {
setProjectDuplicateData &&
setProjectDuplicateData({
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);
}
break;
default:
break;
break;
case "restore":
if (handleRestoreProject) {
await handleRestoreProject(projectId);
}
setIsKebabOpen(false);
};
OuterClick({
contextClassName: ["kebab-wrapper", "kebab-options-wrapper"],
setMenuVisible: () => setIsKebabOpen(false),
});
const handleProjectName = async (projectName: string) => {
setRenameValue(projectName);
if (!projectId) return;
break;
case "open in new tab":
try {
const projects = await getAllProjects(userId, organization);
if (!projects || !projects.Projects) return;
let projectUuid = projects.Projects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
if (active === "shared" && createdBy) {
const newTab = await viewProject(
organization,
projectId,
createdBy?._id
);
const updateProjects = {
projectId: projectUuid,
organization,
userId,
projectName,
thumbnail: undefined,
};
} else {
const newTab = await viewProject(organization, projectId, userId);
if (projectSocket) {
projectSocket.emit("v1:project:update", updateProjects);
}
} catch (error) { }
};
function getRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const intervals: Record<RelativeTimeFormatUnit, number> = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
for (const key in intervals) {
const unit = key as RelativeTimeFormatUnit;
const diff = Math.floor(diffInSeconds / intervals[unit]);
if (diff >= 1) {
return rtf.format(-diff, unit);
}
setProjectName(projectName);
setIsKebabOpen(false);
}
} catch (error) {}
window.open(`/${projectId}`, "_blank");
break;
case "rename":
setIsRenaming(true);
break;
case "duplicate":
if (handleDuplicateWorkspaceProject) {
setProjectDuplicateData &&
setProjectDuplicateData({
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);
}
return "just now";
break;
default:
break;
}
setIsKebabOpen(false);
};
const kebabOptionsMap: Record<string, string[]> = {
default: ["rename", "delete", "duplicate", "open in new tab"],
trash: ["restore", "delete"],
shared: ["duplicate", "open in new tab"],
OuterClick({
contextClassName: ["kebab-wrapper", "kebab-options-wrapper"],
setMenuVisible: () => setIsKebabOpen(false),
});
const handleProjectName = async (projectName: string) => {
setRenameValue(projectName);
if (!projectId) return;
try {
const projects = await getAllProjects(userId, organization);
if (!projects || !projects.Projects) return;
let projectUuid = projects.Projects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
);
const updateProjects = {
projectId: projectUuid?._id,
organization,
userId,
projectName,
thumbnail: undefined,
};
// const updatedProjectName = await updateProject(
// projectUuid._id,
// userId,
// organization,
// undefined,
// projectName
// );
if (projectSocket) {
projectSocket.emit("v1:project:update", updateProjects);
}
} catch (error) {}
};
function getRelativeTime(dateString: string): string {
const date = new Date(dateString);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const intervals: Record<RelativeTimeFormatUnit, number> = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};
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;
};
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
return (
<button
className="dashboard-card-container"
onClick={navigateToProject}
title={projectName}
onMouseLeave={() => setIsKebabOpen(false)}
for (const key in intervals) {
const unit = key as RelativeTimeFormatUnit;
const diff = Math.floor(diffInSeconds / intervals[unit]);
if (diff >= 1) {
return rtf.format(-diff, unit);
}
}
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"
onClick={navigateToProject}
title={projectName}
onMouseLeave={() => setIsKebabOpen(false)}
>
<div className="dashboard-card-wrapper">
<div className="preview-container">
{thumbnail ? (
<img src={thumbnail} alt="" />
) : (
<img src={img} alt="" />
)}
</div>
<div
className="project-details-container"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="dashboard-card-wrapper">
<div className="preview-container">
{thumbnail ? <img src={thumbnail} alt="" /> : <img src={img} alt="" />}
</div>
<div className="project-details-container" onClick={(e) => { e.stopPropagation() }}>
<div className="project-details">
{isRenaming ? (
<input
value={renameValue}
onChange={(e) => {
e.stopPropagation();
handleProjectName(e.target.value);
}}
onBlur={() => {
setIsRenaming(false);
setProjectName(renameValue);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setProjectName(renameValue);
setIsRenaming(false);
}
}}
autoFocus
/>
) : (
<span>{renameValue}</span>
)}
{createdAt && (
<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">
{(!createdBy) ? userName ? userName?.charAt(0).toUpperCase() : "A" : createdBy?.userName?.charAt(0).toUpperCase()}
</div>
<button
className="kebab-wrapper"
onClick={(e) => {
e.stopPropagation();
setIsKebabOpen((prev) => !prev);
}}
>
<KebabIcon />
</button>
</div>
</div>
</div>
{isKebabOpen && (
<div className="kebab-options-wrapper">
{getOptions().map((option) => (
<button
key={option}
className="option"
onClick={(e) => {
e.stopPropagation();
handleOptionClick(option);
}}
>
{option}
</button>
))}
</div>
<div className="project-details">
{isRenaming ? (
<input
value={renameValue}
onChange={(e) => {
e.stopPropagation();
handleProjectName(e.target.value);
}}
onBlur={() => {
setIsRenaming(false);
setProjectName(renameValue);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setProjectName(renameValue);
setIsRenaming(false);
}
}}
autoFocus
/>
) : (
<span>{renameValue}</span>
)}
</button>
);
{createdAt && (
<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">
{!createdBy
? userName
? userName?.charAt(0).toUpperCase()
: "A"
: createdBy?.userName?.charAt(0).toUpperCase()}
</div>
<button
className="kebab-wrapper"
onClick={(e) => {
e.stopPropagation();
setIsKebabOpen((prev) => !prev);
}}
>
<KebabIcon />
</button>
</div>
</div>
</div>
{isKebabOpen && (
<div className="kebab-options-wrapper">
{getOptions().map((option) => (
<button
key={option}
className="option"
onClick={(e) => {
e.stopPropagation();
handleOptionClick(option);
}}
>
{option}
</button>
))}
</div>
)}
</button>
);
};
export default DashboardCard;
export default DashboardCard;

View File

@@ -8,15 +8,16 @@ import { recentlyViewed } from "../../services/dashboard/recentlyViewed";
import { searchProject } from "../../services/dashboard/searchProjects";
import { deleteProject } from "../../services/dashboard/deleteProject";
import ProjectSocketRes from "./socket/projectSocketRes.dev";
import { generateUniqueId } from "../../functions/generateUniqueId";
interface Project {
_id: string;
projectName: string;
thumbnail: string;
createdBy: { _id: string, userName: string };
createdBy: { _id: string; userName: string };
projectUuid?: string;
createdAt: string;
isViewed?: string
isViewed?: string;
}
interface RecentProjectsData {
@@ -25,12 +26,12 @@ interface RecentProjectsData {
const DashboardHome: React.FC = () => {
const [recentProjects, setRecentProjects] = useState<RecentProjectsData>({});
const [isSearchActive, setIsSearchActive] = useState<boolean>(false);
const { userId, organization } = getUserData();
const { projectSocket } = useSocketStore();
const [recentDuplicateData, setRecentDuplicateData] = useState<Object>({});
const fetchRecentProjects = async () => {
try {
const projects = await recentlyViewed(organization, userId);
@@ -38,9 +39,7 @@ const DashboardHome: React.FC = () => {
if (JSON.stringify(projects) !== JSON.stringify(recentProjects)) {
setRecentProjects(projects);
}
} catch (error) {
console.error("Error fetching recent projects:", error);
}
} catch (error) {}
};
const handleRecentProjectSearch = async (inputValue: string) => {
@@ -65,7 +64,7 @@ const DashboardHome: React.FC = () => {
// userId,
// organization
// );
// console.log('deletedProject: ', deletedProject);
//
//socket for delete Project
const deleteProject = {
@@ -91,9 +90,7 @@ const DashboardHome: React.FC = () => {
};
});
setIsSearchActive(false);
} catch (error) {
console.error("Error deleting project:", error);
}
} catch (error) {}
};
const handleDuplicateRecentProject = async (
@@ -105,7 +102,8 @@ const DashboardHome: React.FC = () => {
userId,
thumbnail,
organization,
projectUuid: projectId,
projectUuid: generateUniqueId(),
refProjectID: projectId,
projectName,
};
projectSocket.emit("v1:project:Duplicate", duplicateRecentProjectData);
@@ -163,4 +161,4 @@ const DashboardHome: React.FC = () => {
);
};
export default DashboardHome;
export default DashboardHome;

View File

@@ -9,6 +9,7 @@ import { deleteProject } from "../../services/dashboard/deleteProject";
import ProjectSocketRes from "./socket/projectSocketRes.dev";
import { sharedWithMeProjects } from "../../services/dashboard/sharedWithMeProject";
import { duplicateProject } from "../../services/dashboard/duplicateProject";
import { generateUniqueId } from "../../functions/generateUniqueId";
interface Project {
_id: string;
@@ -27,7 +28,7 @@ const DashboardProjects: React.FC = () => {
const [workspaceProjects, setWorkspaceProjects] = useState<WorkspaceProjects>(
{}
);
const [sharedwithMeProject, setSharedWithMeProjects] = useState<any>([])
const [sharedwithMeProject, setSharedWithMeProjects] = useState<any>([]);
const [projectDuplicateData, setProjectDuplicateData] = useState<Object>({});
const [isSearchActive, setIsSearchActive] = useState<boolean>(false);
const [activeFolder, setActiveFolder] = useState<string>("myProjects");
@@ -41,7 +42,11 @@ const DashboardProjects: React.FC = () => {
}
if (!setWorkspaceProjects || !setIsSearchActive) return;
const searchedProject = await searchProject(organization, userId, inputValue);
const searchedProject = await searchProject(
organization,
userId,
inputValue
);
setIsSearchActive(true);
setWorkspaceProjects(searchedProject.message ? {} : searchedProject);
};
@@ -49,14 +54,13 @@ const DashboardProjects: React.FC = () => {
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);
}
} catch (error) {}
};
const handleDeleteProject = async (projectId: any) => {
@@ -66,7 +70,7 @@ const DashboardProjects: React.FC = () => {
// userId,
// organization
// );
// console.log('deletedProject: ', deletedProject);
//
const deleteProjects = {
projectId,
organization,
@@ -77,7 +81,6 @@ const DashboardProjects: React.FC = () => {
if (projectSocket) {
projectSocket.emit("v1:project:delete", deleteProjects);
} else {
console.error("Socket is not connected.");
}
setWorkspaceProjects((prevDiscardedProjects: WorkspaceProjects) => {
if (!Array.isArray(prevDiscardedProjects?.Projects)) {
@@ -92,22 +95,28 @@ const DashboardProjects: React.FC = () => {
};
});
setIsSearchActive(false);
} catch (error) {
console.error("Error deleting project:", error);
}
} catch (error) {}
};
const handleDuplicateWorkspaceProject = async (
projectId: string,
projectName: string,
thumbnail: string,
thumbnail: string
) => {
// const duplicatedProject = await duplicateProject(
// projectId,
// generateUniqueId(),
// thumbnail,
// projectName
// );
// console.log("duplicatedProject: ", duplicatedProject);
const duplicateProjectData = {
userId,
thumbnail,
organization,
projectUuid: projectId,
projectUuid: generateUniqueId(),
refProjectID: projectId,
projectName,
};
projectSocket.emit("v1:project:Duplicate", duplicateProjectData);
@@ -153,16 +162,12 @@ const DashboardProjects: React.FC = () => {
));
};
const sharedProject = async () => {
try {
const sharedWithMe = await sharedWithMeProjects();
setSharedWithMeProjects(sharedWithMe)
} catch {
}
}
setSharedWithMeProjects(sharedWithMe);
} catch {}
};
useEffect(() => {
if (!isSearchActive) {
@@ -172,10 +177,9 @@ const DashboardProjects: React.FC = () => {
useEffect(() => {
if (activeFolder === "shared") {
sharedProject()
sharedProject();
}
}, [activeFolder])
}, [activeFolder]);
return (
<div className="dashboard-home-container">
@@ -184,7 +188,10 @@ const DashboardProjects: React.FC = () => {
handleProjectsSearch={handleProjectsSearch}
/>
<div className="dashboard-container" style={{ height: "calc(100% - 87px)" }}>
<div
className="dashboard-container"
style={{ height: "calc(100% - 87px)" }}
>
<div className="header-wrapper" style={{ display: "flex", gap: "7px" }}>
<button
className={`header ${activeFolder === "myProjects" && "active"}`}
@@ -199,19 +206,22 @@ const DashboardProjects: React.FC = () => {
Shared with me
</button>
</div>
<div className="cards-container">{activeFolder == "myProjects" ? renderProjects() : renderSharedProjects()}</div>
<div className="cards-container">
{activeFolder == "myProjects"
? renderProjects()
: renderSharedProjects()}
</div>
{projectDuplicateData && Object.keys(projectDuplicateData).length > 0 && (
<ProjectSocketRes
setIsSearchActive={setIsSearchActive}
setWorkspaceProjects={setWorkspaceProjects}
/>
)}
{projectDuplicateData &&
Object.keys(projectDuplicateData).length > 0 && (
<ProjectSocketRes
setIsSearchActive={setIsSearchActive}
setWorkspaceProjects={setWorkspaceProjects}
/>
)}
</div>
</div>
);
};
export default DashboardProjects;

View File

@@ -160,3 +160,153 @@ export function RenameIcon() {
</svg>
);
}
export function FocusIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.31999 1.56H9.89999C10.0325 1.56 10.14 1.66745 10.14 1.8V4.14M10.14 7.5V9.9C10.14 10.0325 10.0325 10.14 9.89999 10.14H7.31999M4.55999 10.14H1.91999C1.78744 10.14 1.67999 10.0325 1.67999 9.9V7.5M1.67999 4.14V1.8C1.67999 1.66745 1.78744 1.56 1.91999 1.56H4.55999" stroke="white" stroke-linecap="round" />
<circle cx="6.00005" cy="5.87999" r="1.7" stroke="white" />
</svg>
);
}
export function TransformIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_55_63)">
<path d="M3 0.75C2.40326 0.75 1.83097 0.987053 1.40901 1.40901C0.987053 1.83097 0.75 2.40326 0.75 3C0.75 3.59674 0.987053 4.16903 1.40901 4.59099C1.83097 5.01295 2.40326 5.25 3 5.25C3.24134 5.24937 3.481 5.20991 3.7098 5.13314L3.28805 4.71141L4.79632 3.20316L4.94545 3.05402L5.22342 3.33199C5.24047 3.22214 5.24935 3.11117 5.25 3C5.25 2.40326 5.01295 1.83097 4.59099 1.40901C4.16903 0.987053 3.59674 0.75 3 0.75ZM4.94545 3.65062L3.88467 4.71141L5.92336 6.75L5.37333 7.30001L8.07427 7.84017L7.53403 5.13923L6.98405 5.68922L4.94545 3.65062ZM8.28647 6.75L8.61202 8.37797L6.75 8.00555V11.25H11.25V6.75H8.28645H8.28647Z" fill="#FCFDFD" />
</g>
<defs>
<clipPath id="clip0_55_63">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function DublicateIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_190)">
<path d="M9 1.5H2C1.72386 1.5 1.5 1.72386 1.5 2V9C1.5 9.27615 1.27614 9.5 1 9.5C0.72386 9.5 0.5 9.27615 0.5 9V2C0.5 1.17158 1.17158 0.5 2 0.5H9C9.27615 0.5 9.5 0.72386 9.5 1C9.5 1.27614 9.27615 1.5 9 1.5Z" fill="white" />
<path d="M6.5 5.5C6.5 5.22385 6.72385 5 7 5C7.27615 5 7.5 5.22385 7.5 5.5V6.5H8.5C8.77615 6.5 9 6.72385 9 7C9 7.27615 8.77615 7.5 8.5 7.5H7.5V8.5C7.5 8.77615 7.27615 9 7 9C6.72385 9 6.5 8.77615 6.5 8.5V7.5H5.5C5.22385 7.5 5 7.27615 5 7C5 6.72385 5.22385 6.5 5.5 6.5H6.5V5.5Z" fill="white" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 2.5C10.8285 2.5 11.5 3.17158 11.5 4V10C11.5 10.8285 10.8285 11.5 10 11.5H4C3.17158 11.5 2.5 10.8285 2.5 10V4C2.5 3.17158 3.17158 2.5 4 2.5H10ZM10 3.5C10.2761 3.5 10.5 3.72386 10.5 4V10C10.5 10.2761 10.2761 10.5 10 10.5H4C3.72386 10.5 3.5 10.2761 3.5 10V4C3.5 3.72386 3.72386 3.5 4 3.5H10Z" fill="white" />
</g>
<defs>
<clipPath id="clip0_1_190">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function CopyIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_197)">
<path d="M4.375 1.5H8.875C9.22018 1.5 9.5 1.77982 9.5 2.125V5.07422C9.25749 5.02497 9.00651 5 8.75 5C6.67893 5 5 6.67893 5 8.75C5 8.91951 5.01426 9.08616 5.03613 9.25H4.375C4.02982 9.25 3.75 8.97018 3.75 8.625V2.125C3.75 1.77982 4.02982 1.5 4.375 1.5Z" stroke="white" />
<path d="M7.02181 10.8891C5.8404 9.93469 5.6564 8.20324 6.61085 7.02182C7.56529 5.84041 9.29675 5.65641 10.4782 6.61086C11.6596 7.5653 11.8436 9.29676 10.8891 10.4782C9.93468 11.6596 8.20322 11.8436 7.02181 10.8891ZM7.53035 9.63652C7.55951 9.73588 7.64818 9.80729 7.7514 9.81511L7.79716 9.81441L7.84067 9.80562C7.94019 9.77642 8.01223 9.68724 8.01987 9.58381L8.01932 9.53942L7.89568 8.38246L9.76272 9.88956L9.80012 9.91475C9.89114 9.96447 10.0045 9.95243 10.083 9.88469L10.1143 9.8522L10.1395 9.8148C10.1892 9.72378 10.1772 9.61043 10.1094 9.53189L10.0769 9.50062L8.20913 7.99291L9.36823 7.86974L9.41174 7.86095C9.52542 7.82759 9.60327 7.71674 9.59039 7.59475C9.57742 7.47272 9.47859 7.38002 9.36039 7.37128L9.3154 7.37259L7.5357 7.5631L7.50592 7.57044L7.46405 7.58808L7.42779 7.61277L7.40435 7.63401L7.37612 7.66895L7.36028 7.69633L7.35134 7.71672L7.33894 7.75692L7.33456 7.781L7.33441 7.81227L7.52217 9.59225L7.53035 9.63652Z" fill="white" />
</g>
<defs>
<clipPath id="clip0_1_197">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function PasteIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_207)">
<path d="M4.375 1.5H8.875C9.22018 1.5 9.5 1.77982 9.5 2.125V5.07422C9.25749 5.02497 9.00651 5 8.75 5C6.67893 5 5 6.67893 5 8.75C5 8.91951 5.01426 9.08616 5.03613 9.25H4.375C4.02982 9.25 3.75 8.97018 3.75 8.625V2.125C3.75 1.77982 4.02982 1.5 4.375 1.5Z" stroke="white" />
<path d="M10.4408 6.58164C11.6383 7.51587 11.8516 9.24395 10.9174 10.4414C9.9832 11.6389 8.25512 11.8523 7.05765 10.918C5.86019 9.98382 5.64679 8.25574 6.58102 7.05828C7.51524 5.86081 9.24332 5.64742 10.4408 6.58164ZM9.95361 7.84272C9.92276 7.74387 9.83289 7.67398 9.72956 7.66791L9.68382 7.66939L9.64046 7.67892C9.54146 7.7098 9.47094 7.8002 9.46506 7.90374L9.46636 7.94811L9.60965 9.10281L7.71726 7.62766L7.67944 7.60311C7.58759 7.55494 7.47446 7.56891 7.39709 7.63798L7.36637 7.67099L7.34182 7.70881C7.29366 7.80066 7.30763 7.91379 7.37669 7.99117L7.4097 8.02188L9.30286 9.49763L8.14603 9.64048L8.10268 9.65001C7.98957 9.6853 7.91362 9.79746 7.92858 9.91921C7.94362 10.041 8.044 10.132 8.16233 10.1387L8.2073 10.1367L9.9835 9.91593L10.0131 9.90809L10.0547 9.88974L10.0906 9.86444L10.1136 9.8428L10.1413 9.80739L10.1566 9.77974L10.1652 9.7592L10.1769 9.71879L10.1809 9.69464L10.1805 9.66338L9.96254 7.88684L9.95361 7.84272Z" fill="white" />
</g>
<defs>
<clipPath id="clip0_1_207">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function ModifiersIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4.5V3.23607C2 3.08082 2.03615 2.92771 2.10558 2.78886L2.5 2H5L5.5 3H10.5C10.7761 3 11 3.22386 11 3.5V4.5V9C11 9.5523 10.5523 10 10 10H9" stroke="white" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8.61805 4.5H1.15458C0.824894 4.5 0.585449 4.81349 0.672199 5.13155L1.79898 9.2631C1.91764 9.6982 2.3128 10 2.76375 10H9.84535C10.175 10 10.4145 9.6865 10.3277 9.36845L9.10045 4.86844C9.0411 4.65091 8.84355 4.5 8.61805 4.5Z" stroke="white" />
</svg>
);
}
export function DeleteIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_241)">
<path d="M4.70588 5.32353V9.02941M7.17647 5.32353V9.02941M9.64706 2.85294V10.2647C9.64706 10.947 9.09402 11.5 8.41177 11.5H3.47059C2.78835 11.5 2.23529 10.947 2.23529 10.2647V2.85294M1 2.85294H10.8824M7.79412 2.85294V2.23529C7.79412 1.55306 7.24108 1 6.55882 1H5.32353C4.6413 1 4.08824 1.55306 4.08824 2.23529V2.85294" stroke="white" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_1_241">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function MoveIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_241)">
<path d="M4.70588 5.32353V9.02941M7.17647 5.32353V9.02941M9.64706 2.85294V10.2647C9.64706 10.947 9.09402 11.5 8.41177 11.5H3.47059C2.78835 11.5 2.23529 10.947 2.23529 10.2647V2.85294M1 2.85294H10.8824M7.79412 2.85294V2.23529C7.79412 1.55306 7.24108 1 6.55882 1H5.32353C4.6413 1 4.08824 1.55306 4.08824 2.23529V2.85294" stroke="white" stroke-linecap="round" stroke-linejoin="round" />
</g>
<defs>
<clipPath id="clip0_1_241">
<rect width="12" height="12" fill="white" />
</clipPath>
</defs>
</svg>
);
}
export function RotateIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
</svg>
);
}
export function GroupIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.99998 1.31111H4.7251L3.88539 0.471406L4.3568 0L6.00124 1.64444L4.37502 3.27067L3.90361 2.79927L4.72511 1.97777H3.99998C2.89541 1.97777 1.99998 2.87321 1.99998 3.97777H1.33331C1.33331 2.50501 2.52722 1.31111 3.99998 1.31111ZM3.99998 5.33333C3.99998 4.59696 4.59693 4 5.33331 4H10.6667C11.4031 4 12 4.59696 12 5.33333V10.6667C12 11.4031 11.4031 12 10.6667 12H5.33331C4.59693 12 3.99998 11.4031 3.99998 10.6667V5.33333ZM5.33331 4.66667H10.6667C11.0349 4.66667 11.3333 4.96514 11.3333 5.33333V10.6667C11.3333 11.0349 11.0349 11.3333 10.6667 11.3333H5.33331C4.96513 11.3333 4.66664 11.0349 4.66664 10.6667V5.33333C4.66664 4.96514 4.96513 4.66667 5.33331 4.66667Z" fill="#FCFDFD" />
</svg>
);
}
export function ArrayIcon() {
return (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.09998" y="0.5" width="1.66667" height="5.66667" rx="0.5" stroke="white" />
<rect x="5.09998" y="3.16797" width="1.66667" height="5.66667" rx="0.5" stroke="white" />
<rect x="9.09998" y="5.83203" width="1.66667" height="5.66667" rx="0.5" stroke="white" />
</svg>
);
}
export function SubMenuIcon() {
return (
<svg width="4" height="6" viewBox="0 0 4 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.5 6V0L4 3L0.5 6Z" fill="white" />
</svg>
);
}

View File

@@ -3,6 +3,7 @@ import {
useLoadingProgress,
useRenameModeStore,
useSaveVersion,
useSelectedAssets,
useSelectedComment,
useSelectedFloorItem,
useSocketStore,
@@ -58,6 +59,7 @@ function MainScene() {
const { setFloatingWidget } = useFloatingWidget();
const { clearComparisonProduct } = useComparisonProduct();
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
const { selectedAssets,setSelectedAssets } = useSelectedAssets();
const { assetStore, productStore } = useSceneContext();
const { products } = productStore();
const { setName } = assetStore();
@@ -97,18 +99,40 @@ function MainScene() {
const handleObjectRename = async (newName: string) => {
if (!projectId) return
let response = await setAssetsApi({
modelUuid: selectedFloorItem.userData.modelUuid,
modelName: newName,
projectId
});
selectedFloorItem.userData = {
...selectedFloorItem.userData,
modelName: newName
};
setSelectedFloorItem(selectedFloorItem);
setIsRenameMode(false);
setName(selectedFloorItem.userData.modelUuid, response.modelName);
if (selectedFloorItem) {
console.log('selectedFloorItem.userData.modelUuid: ', selectedFloorItem.userData.modelUuid);
console.log(' newName: ', newName);
console.log('projectId: ', projectId);
setAssetsApi({
modelUuid: selectedFloorItem.userData.modelUuid,
modelName: newName,
projectId,
versionId: selectedVersion?.versionId || ''
}).then(() => {
selectedFloorItem.userData = {
...selectedFloorItem.userData,
modelName: newName
};
setSelectedFloorItem(selectedFloorItem);
setIsRenameMode(false);
setName(selectedFloorItem.userData.modelUuid, newName);
})
} else if (selectedAssets.length === 1) {
setAssetsApi({
modelUuid: selectedAssets[0].userData.modelUuid,
modelName: newName,
projectId,
versionId: selectedVersion?.versionId || ''
}).then(() => {
selectedAssets[0].userData = {
...selectedAssets[0].userData,
modelName: newName
};
setSelectedAssets(selectedAssets);
setIsRenameMode(false);
setName(selectedAssets[0].userData.modelUuid, newName);
})
}
}
return (
@@ -135,7 +159,7 @@ function MainScene() {
{(isPlaying) &&
activeModule !== "simulation" && <ControlsPlayer />}
{isRenameMode && selectedFloorItem?.userData.modelName && <RenameTooltip name={selectedFloorItem?.userData.modelName} onSubmit={handleObjectRename} />}
{isRenameMode && (selectedFloorItem?.userData.modelName || selectedAssets.length === 1) && <RenameTooltip name={selectedFloorItem?.userData.modelName || selectedAssets[0].userData.modelName} onSubmit={handleObjectRename} />}
{/* remove this later */}
{activeModule === "builder" && !toggleThreeD && <SelectFloorPlan />}
</>
@@ -188,7 +212,7 @@ function MainScene() {
{activeModule !== "market" && !selectedUser && <Footer />}
<VersionSaved />
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat/>}
{(commentPositionState !== null || selectedComment !== null) && <ThreadChat />}
</>
);

View File

@@ -17,255 +17,242 @@ import SkeletonUI from "../../templates/SkeletonUI";
// -------------------------------------
interface AssetProp {
filename: string;
thumbnail?: string;
category: string;
description?: string;
tags: string;
url?: string;
uploadDate?: number;
isArchieve?: boolean;
animated?: boolean;
price?: number;
CreatedBy?: string;
filename: string;
thumbnail?: string;
category: string;
description?: string;
tags: string;
url?: string;
uploadDate?: number;
isArchieve?: boolean;
animated?: boolean;
price?: number;
CreatedBy?: string;
}
interface CategoryListProp {
assetImage?: string;
assetName?: string;
categoryImage: string;
category: string;
assetImage?: string;
assetName?: string;
categoryImage: string;
category: string;
}
const Assets: React.FC = () => {
const { setSelectedItem } = useSelectedItem();
const [searchValue, setSearchValue] = useState<string>("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [categoryAssets, setCategoryAssets] = useState<AssetProp[]>([]);
const [filtereredAssets, setFiltereredAssets] = useState<AssetProp[]>([]);
const [categoryList, setCategoryList] = useState<CategoryListProp[]>([]);
const [isLoading, setisLoading] = useState<boolean>(false); // Loading state for assets
const { setSelectedItem } = useSelectedItem();
const [searchValue, setSearchValue] = useState<string>("");
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const [categoryAssets, setCategoryAssets] = useState<AssetProp[]>([]);
const [filtereredAssets, setFiltereredAssets] = useState<AssetProp[]>([]);
const [categoryList, setCategoryList] = useState<CategoryListProp[]>([]);
const [isLoading, setisLoading] = useState<boolean>(false); // Loading state for assets
const handleSearchChange = (value: string) => {
const searchTerm = value.toLowerCase();
setSearchValue(value);
if (searchTerm.trim() === "" && !selectedCategory) {
setCategoryAssets([]);
return;
}
const filteredModels = filtereredAssets?.filter((model) => {
if (!model?.tags || !model?.filename || !model?.category) return false;
if (searchTerm.startsWith(":") && searchTerm.length > 1) {
const tagSearchTerm = searchTerm.slice(1);
return model.tags.toLowerCase().includes(tagSearchTerm);
} else if (selectedCategory) {
return (
model.category
.toLowerCase()
.includes(selectedCategory.toLowerCase()) &&
model.filename.toLowerCase().includes(searchTerm)
);
} else {
return model.filename.toLowerCase().includes(searchTerm);
}
});
const handleSearchChange = (value: string) => {
const searchTerm = value.toLowerCase();
setSearchValue(value);
if (searchTerm.trim() === "" && !selectedCategory) {
setCategoryAssets([]);
return;
}
const filteredModels = filtereredAssets?.filter((model) => {
if (!model?.tags || !model?.filename || !model?.category) return false;
if (searchTerm.startsWith(":") && searchTerm.length > 1) {
const tagSearchTerm = searchTerm.slice(1);
return model.tags.toLowerCase().includes(tagSearchTerm);
} else if (selectedCategory) {
return (
model.category
.toLowerCase()
.includes(selectedCategory.toLowerCase()) &&
model.filename.toLowerCase().includes(searchTerm)
);
} else {
return model.filename.toLowerCase().includes(searchTerm);
}
});
setCategoryAssets(filteredModels);
};
useEffect(() => {
const filteredAssets = async () => {
try {
const filt = await fetchAssets();
setFiltereredAssets(filt);
} catch {
echo.error("Filter asset not found");
}
setCategoryAssets(filteredModels);
};
filteredAssets();
}, [categoryAssets]);
useEffect(() => {
setCategoryList([
{ category: "Fenestration", categoryImage: feneration },
{ category: "Vehicles", categoryImage: vehicle },
{ category: "Workstation", categoryImage: workStation },
{ category: "Machines", categoryImage: machines },
{ category: "Workers", categoryImage: worker },
{ category: "Storage", categoryImage: storage },
{ category: "Safety", categoryImage: safety },
{ category: "Office", categoryImage: office },
]);
}, []);
const fetchCategoryAssets = async (asset: any) => {
setisLoading(true);
setSelectedCategory(asset);
try {
const res = await getCategoryAsset(asset);
setCategoryAssets(res);
setFiltereredAssets(res);
setisLoading(false); // End loading
// eslint-disable-next-line
} catch (error) {
echo.error("failed to fetch assets");
setisLoading(false);
}
};
return (
<div className="assets-container-main">
<Search onChange={handleSearchChange} />
<div className="assets-list-section">
<section>
{(() => {
if (isLoading) {
return <SkeletonUI type="asset" />; // Show skeleton when loading
useEffect(() => {
const filteredAssets = async () => {
try {
const filt = await fetchAssets();
setFiltereredAssets(filt);
} catch {
echo.error("Filter asset not found");
}
if (searchValue) {
return (
<div className="assets-result">
<div className="assets-wrapper">
<div className="searched-content">
<p>Results for {searchValue}</p>
</div>
<div className="assets-container">
{categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset.filename}`}
className="assets"
id={asset.filename}
title={asset.filename}
>
<img
src={asset?.thumbnail}
alt={asset.filename}
className="asset-image"
onPointerDown={() => {
setSelectedItem({
name: asset.filename,
id: asset.AssetID,
type:
asset.type === "undefined"
? undefined
: asset.type
});
}}
/>
};
filteredAssets();
}, [categoryAssets]);
<div className="asset-name">
{asset.filename
.split("_")
.map(
(word: any) =>
word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</div>
</div>
))}
</div>
</div>
</div>
);
}
useEffect(() => {
setCategoryList([
{ category: "Fenestration", categoryImage: feneration },
{ category: "Vehicles", categoryImage: vehicle },
{ category: "Workstation", categoryImage: workStation },
{ category: "Machines", categoryImage: machines },
{ category: "Workers", categoryImage: worker },
{ category: "Storage", categoryImage: storage },
{ category: "Safety", categoryImage: safety },
{ category: "Office", categoryImage: office },
]);
}, []);
if (selectedCategory) {
return (
<div className="assets-wrapper">
<h2>
{selectedCategory}
<button
className="back-button"
id="asset-backButtom"
onClick={() => {
setSelectedCategory(null);
setCategoryAssets([]);
}}
>
Back
</button>
</h2>
<div className="assets-container">
{categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset}`}
className="assets"
id={asset.filename}
title={asset.filename}
>
<img
src={asset?.thumbnail}
alt={asset.filename}
className="asset-image"
onPointerDown={() => {
setSelectedItem({
name: asset.filename,
id: asset.AssetID,
type:
asset.type === "undefined"
? undefined
: asset.type,
category: asset.category,
subCategory: asset.subCategory
});
}}
/>
<div className="asset-name">
{asset.filename
.split("_")
.map(
(word: any) =>
word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</div>
</div>
))}
{categoryAssets.length === 0 && (
<div className="no-asset">
🚧 The asset shelf is empty. We're working on filling it
up!
</div>
)}
</div>
</div>
);
}
const fetchCategoryAssets = async (asset: any) => {
setisLoading(true);
setSelectedCategory(asset);
try {
const res = await getCategoryAsset(asset);
setCategoryAssets(res);
setFiltereredAssets(res);
setisLoading(false); // End loading
// eslint-disable-next-line
} catch (error) {
echo.error("failed to fetch assets");
setisLoading(false);
}
};
return (
<div className="assets-wrapper">
<h2>Categories</h2>
<div className="categories-container">
{Array.from(
new Set(categoryList.map((asset) => asset.category))
).map((category, index) => {
const categoryInfo = categoryList.find(
(asset) => asset.category === category
);
return (
<div
key={`${index}-${category}`}
className="category"
id={category}
onClick={() => fetchCategoryAssets(category)}
>
<img
src={categoryInfo?.categoryImage ?? ""}
alt={category}
className="category-image"
draggable={false}
/>
<div className="category-name">{category}</div>
</div>
);
})}
</div>
</div>
);
})()}
</section>
</div>
</div>
);
return (
<div className="assets-container-main">
<Search onChange={handleSearchChange} />
<div className="assets-list-section">
<section>
{(() => {
if (isLoading) {
return <SkeletonUI type="asset" />; // Show skeleton when loading
}
if (searchValue) {
return (
<div className="assets-result">
<div className="assets-wrapper">
<div className="searched-content">
<p>Results for {searchValue}</p>
</div>
<div className="assets-container">
{categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset.filename}`}
className="assets"
id={asset.filename}
title={asset.filename}
>
<img
src={asset?.thumbnail}
alt={asset.filename}
className="asset-image"
onPointerDown={() => {
setSelectedItem({
name: asset.filename,
id: asset.AssetID,
type: asset.type === "undefined" ? undefined : asset.type
});
}}
/>
<div className="asset-name">
{asset.filename
.split("_")
.map(
(word: any) =>
word.charAt(0).toUpperCase() + word.slice(1)
)
.join(" ")}
</div>
</div>
))}
</div>
</div>
</div>
);
}
if (selectedCategory) {
return (
<div className="assets-wrapper">
<h2>
{selectedCategory}
<button
className="back-button"
id="asset-backButtom"
onClick={() => {
setSelectedCategory(null);
setCategoryAssets([]);
}}
>
Back
</button>
</h2>
<div className="assets-container">
{categoryAssets?.map((asset: any, index: number) => (
<div
key={`${index}-${asset}`}
className="assets"
id={asset.filename}
title={asset.filename}
>
<img
src={asset?.thumbnail}
alt={asset.filename}
className="asset-image"
onPointerDown={() => {
setSelectedItem({
name: asset.filename,
id: asset.AssetID,
type: asset.type === "undefined" ? undefined : asset.type,
category: asset.category,
subType: asset.subType
});
}}
/>
<div className="asset-name">
{asset.filename.split("_").map((word: any) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ")}
</div>
</div>
))}
{categoryAssets.length === 0 && (
<div className="no-asset">
🚧 The asset shelf is empty. We're working on filling it up!
</div>
)}
</div>
</div>
);
}
return (
<div className="assets-wrapper">
<h2>Categories</h2>
<div className="categories-container">
{Array.from(
new Set(categoryList.map((asset) => asset.category))
).map((category, index) => {
const categoryInfo = categoryList.find(
(asset) => asset.category === category
);
return (
<div
key={`${index}-${category}`}
className="category"
id={category}
onClick={() => fetchCategoryAssets(category)}
>
<img
src={categoryInfo?.categoryImage ?? ""}
alt={category}
className="category-image"
draggable={false}
/>
<div className="category-name">{category}</div>
</div>
);
})}
</div>
</div>
);
})()}
</section>
</div>
</div>
);
};
export default Assets;

View File

@@ -1,23 +1,62 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import Search from "../../ui/inputs/Search";
import DropDownList from "../../ui/list/DropDownList";
import { useSceneContext } from "../../../modules/scene/sceneContext";
import { isPointInsidePolygon } from "../../../functions/isPointInsidePolygon";
interface ZoneData {
id: string;
name: string;
assets: { id: string; name: string; position?: []; rotation?: {} }[];
}
const Outline: React.FC = () => {
const [searchValue, setSearchValue] = useState<string>("");
const [zoneDataList, setZoneDataList] = useState<ZoneData[]>([]);
const [buildingsList, setBuildingsList] = useState<{ id: string; name: string }[]>([]);
const [isLayersOpen, setIsLayersOpen] = useState(true);
const [isBuildingsOpen, setIsBuildingsOpen] = useState(false);
const [isZonesOpen, setIsZonesOpen] = useState(false);
const { assetStore, zoneStore } = useSceneContext();
const { assets } = assetStore();
const { zones } = zoneStore();
useEffect(() => {
const updatedZoneList: ZoneData[] = zones?.map((zone: any) => {
const polygon2D = zone.points.map((p: any) => [p.position[0], p.position[2]]);
const assetsInZone = assets.filter((item: any) => {
const [x, , z] = item.position;
return isPointInsidePolygon([x, z], polygon2D as [number, number][]);
})
.map((item: any) => ({
id: item.modelUuid,
name: item.modelName,
position: item.position,
rotation: item.rotation,
}));
return {
id: zone.zoneUuid,
name: zone.zoneName,
assets: assetsInZone,
};
});
setZoneDataList(updatedZoneList);
}, [zones, assets]);
const handleSearchChange = (value: string) => {
setSearchValue(value);
// console.log(value); // Log the search value if needed
};
const dropdownItems = [
{ id: "1", name: "Ground Floor", active: true },
// { id: "2", name: "Floor 1" },
]; // Example dropdown items
const dropdownItems = [{ id: "1", name: "Ground Floor" }];
return (
<div className="outline-container">
<Search onChange={handleSearchChange} />
{searchValue ? (
<div className="searched-content">
<p>Results for "{searchValue}"</p>
@@ -28,7 +67,8 @@ const Outline: React.FC = () => {
<DropDownList
value="Layers"
items={dropdownItems}
defaultOpen={true}
isOpen={isLayersOpen}
onToggle={() => setIsLayersOpen((prev) => !prev)}
showKebabMenu={false}
showFocusIcon={true}
remove
@@ -36,10 +76,18 @@ const Outline: React.FC = () => {
</section>
<section className="outline-section overflow">
<DropDownList
value="Scene"
items={dropdownItems}
defaultOpen={true}
listType="outline"
value="Buildings"
items={buildingsList}
isOpen={isBuildingsOpen}
onToggle={() => setIsBuildingsOpen((prev) => !prev)}
showKebabMenu={false}
showAddIcon={false}
/>
<DropDownList
value="Zones"
items={zoneDataList}
isOpen={isZonesOpen}
onToggle={() => setIsZonesOpen((prev) => !prev)}
showKebabMenu={false}
showAddIcon={false}
/>

View File

@@ -42,11 +42,11 @@ const ZoneProperties: React.FC = () => {
let response = await zoneCameraUpdate(zonesdata, organization, projectId, selectedVersion?.versionId || "");
// console.log('response: ', response);
//
if (response.message === "zone updated") {
setEdit(false);
} else {
// console.log(response);
//
}
} catch (error) {
echo.error("Failed to set zone view");
@@ -75,7 +75,7 @@ const ZoneProperties: React.FC = () => {
// )
// );
} else {
// console.log(response?.message);
//
}
}
function handleVectorChange(
@@ -85,7 +85,7 @@ const ZoneProperties: React.FC = () => {
setSelectedZone((prev) => ({ ...prev, [key]: newValue }));
}
const checkZoneNameDuplicate = (name: string) => {
console.log('zones: ', zones);
return zones.some(
(zone: any) =>
zone.zoneName?.trim().toLowerCase() === name?.trim().toLowerCase() &&

View File

@@ -15,6 +15,7 @@ import { useProductContext } from "../../../../../modules/simulation/products/pr
import { useParams } from "react-router-dom";
import { useVersionContext } from "../../../../../modules/builder/version/versionContext";
import { useSceneContext } from "../../../../../modules/scene/sceneContext";
import CraneMechanics from "./mechanics/craneMechanics";
const EventProperties: React.FC = () => {
const { selectedEventData } = useSelectedEventData();
@@ -63,6 +64,8 @@ const EventProperties: React.FC = () => {
return "storageUnit";
case "human":
return "human";
case "crane":
return "crane";
default:
return null;
}
@@ -83,6 +86,7 @@ const EventProperties: React.FC = () => {
{assetType === "machine" && <MachineMechanics />}
{assetType === "storageUnit" && <StorageMechanics />}
{assetType === "human" && <HumanMechanics />}
{assetType === "crane" && <CraneMechanics />}
</>
)}
{!currentEventData && selectedEventSphere && (

View File

@@ -1,7 +1,7 @@
import React from "react";
const DefaultAction:React.FC = () => {
return <></>;
};
export default DefaultAction;
import React from "react";
const DefaultAction:React.FC = () => {
return <></>;
};
export default DefaultAction;

View File

@@ -1,10 +1,10 @@
import React from "react";
const DespawnAction: React.FC = () => {
return (
<>
</>
);
};
export default DespawnAction;
import React from "react";
const DespawnAction: React.FC = () => {
return (
<>
</>
);
};
export default DespawnAction;

View File

@@ -1,29 +1,29 @@
import React from "react";
interface PickAndPlaceActionProps {
clearPoints: () => void;
}
const PickAndPlaceAction: React.FC<PickAndPlaceActionProps> = ({
clearPoints,
}) => {
return (
<div className="selected-actions-list">
<div className="value-field-container">
<div className="label">Reset</div>
<button
id="pick-and-place-action-clear-button"
type="button"
className="regularDropdown-container"
onClick={() => {
clearPoints();
}}
>
Clear
</button>
</div>
</div>
);
};
export default PickAndPlaceAction;
import React from "react";
interface PickAndPlaceActionProps {
clearPoints: () => void;
}
const PickAndPlaceAction: React.FC<PickAndPlaceActionProps> = ({
clearPoints,
}) => {
return (
<div className="selected-actions-list">
<div className="value-field-container">
<div className="label">Reset</div>
<button
id="pick-and-place-action-clear-button"
type="button"
className="regularDropdown-container"
onClick={() => {
clearPoints();
}}
>
Clear
</button>
</div>
</div>
);
};
export default PickAndPlaceAction;

View File

@@ -0,0 +1,39 @@
import React from "react";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
interface WorkerActionProps {
loadCount: {
value: number;
min: number;
max: number;
step: number;
defaultValue: string,
disabled: false,
onChange: (value: number) => void;
};
}
const WorkerAction: React.FC<WorkerActionProps> = ({
loadCount
}) => {
return (
<div className="pillarJib-action-container">
{loadCount && (
<InputWithDropDown
label="Max Load Count"
value={loadCount.value.toString()}
min={loadCount.min}
max={loadCount.max}
disabled={loadCount.disabled}
defaultValue={loadCount.defaultValue}
step={loadCount.step}
activeOption="unit"
onClick={() => { }}
onChange={(value) => loadCount.onChange(parseInt(value))}
/>
)}
</div>
);
};
export default WorkerAction;

View File

@@ -1,48 +1,48 @@
import React from "react";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
import SwapAction from "./SwapAction";
interface ProcessActionProps {
value: string;
min: number;
max: number;
defaultValue: string;
onChange: (value: string) => void;
swapOptions: string[];
swapDefaultOption: string;
onSwapSelect: (value: string) => void;
}
const ProcessAction: React.FC<ProcessActionProps> = ({
value,
min,
max,
defaultValue,
onChange,
swapOptions,
swapDefaultOption,
onSwapSelect,
}) => {
return (
<>
<InputWithDropDown
label="Process Time"
value={value}
min={min}
step={1}
max={max}
defaultValue={defaultValue}
activeOption="s"
onClick={() => { }}
onChange={onChange}
/>
<SwapAction
options={swapOptions}
defaultOption={swapDefaultOption}
onSelect={onSwapSelect}
/>
</>
);
};
export default ProcessAction;
import React from "react";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
import SwapAction from "./SwapAction";
interface ProcessActionProps {
value: string;
min: number;
max: number;
defaultValue: string;
onChange: (value: string) => void;
swapOptions: string[];
swapDefaultOption: string;
onSwapSelect: (value: string) => void;
}
const ProcessAction: React.FC<ProcessActionProps> = ({
value,
min,
max,
defaultValue,
onChange,
swapOptions,
swapDefaultOption,
onSwapSelect,
}) => {
return (
<>
<InputWithDropDown
label="Process Time"
value={value}
min={min}
step={1}
max={max}
defaultValue={defaultValue}
activeOption="s"
onClick={() => { }}
onChange={onChange}
/>
<SwapAction
options={swapOptions}
defaultOption={swapDefaultOption}
onSelect={onSwapSelect}
/>
</>
);
};
export default ProcessAction;

View File

@@ -1,72 +1,72 @@
import React from "react";
import PreviewSelectionWithUpload from "../../../../../ui/inputs/PreviewSelectionWithUpload";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
interface SpawnActionProps {
onChangeInterval: (value: string) => void;
onChangeCount: (value: string) => void;
defaultOption: string;
options: string[];
onSelect: (option: string) => void;
intervalValue: string;
countValue: string;
intervalMin: number;
intervalMax: number;
intervalDefaultValue: string;
countMin: number;
countMax: number;
countDefaultValue: string;
}
const SpawnAction: React.FC<SpawnActionProps> = ({
onChangeInterval,
onChangeCount,
defaultOption,
options,
onSelect,
intervalValue,
countValue,
intervalMin,
intervalMax,
intervalDefaultValue,
countMin,
countMax,
countDefaultValue,
}) => {
return (
<>
<InputWithDropDown
label="Spawn interval"
value={intervalValue}
min={intervalMin}
step={1}
defaultValue={intervalDefaultValue}
max={intervalMax}
activeOption="s"
onClick={() => { }}
onChange={onChangeInterval}
/>
<InputWithDropDown
label="Spawn count"
value={countValue}
min={countMin}
step={1}
defaultValue={countDefaultValue}
max={countMax}
activeOption="s"
onClick={() => { }}
onChange={onChangeCount}
/>
{/* <PreviewSelectionWithUpload /> */}
<LabledDropdown
label="Presets"
defaultOption={defaultOption}
options={options}
onSelect={onSelect}
/>
</>
);
};
export default SpawnAction;
import React from "react";
import PreviewSelectionWithUpload from "../../../../../ui/inputs/PreviewSelectionWithUpload";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
interface SpawnActionProps {
onChangeInterval: (value: string) => void;
onChangeCount: (value: string) => void;
defaultOption: string;
options: string[];
onSelect: (option: string) => void;
intervalValue: string;
countValue: string;
intervalMin: number;
intervalMax: number;
intervalDefaultValue: string;
countMin: number;
countMax: number;
countDefaultValue: string;
}
const SpawnAction: React.FC<SpawnActionProps> = ({
onChangeInterval,
onChangeCount,
defaultOption,
options,
onSelect,
intervalValue,
countValue,
intervalMin,
intervalMax,
intervalDefaultValue,
countMin,
countMax,
countDefaultValue,
}) => {
return (
<>
<InputWithDropDown
label="Spawn interval"
value={intervalValue}
min={intervalMin}
step={1}
defaultValue={intervalDefaultValue}
max={intervalMax}
activeOption="s"
onClick={() => { }}
onChange={onChangeInterval}
/>
<InputWithDropDown
label="Spawn count"
value={countValue}
min={countMin}
step={1}
defaultValue={countDefaultValue}
max={countMax}
activeOption="s"
onClick={() => { }}
onChange={onChangeCount}
/>
{/* <PreviewSelectionWithUpload /> */}
<LabledDropdown
label="Presets"
defaultOption={defaultOption}
options={options}
onSelect={onSelect}
/>
</>
);
};
export default SpawnAction;

View File

@@ -1,57 +1,57 @@
import React from "react";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
interface StorageActionProps {
type: "store" | "spawn" | "default";
value: string;
min: number;
max?: number;
defaultValue: string;
currentMaterialType: string;
handleCapacityChange: (value: string) => void;
handleMaterialTypeChange: (value: string) => void;
}
const StorageAction: React.FC<StorageActionProps> = ({ type, value, min, max, defaultValue, currentMaterialType, handleCapacityChange, handleMaterialTypeChange }) => {
return (
<>
{type === 'store' &&
<InputWithDropDown
label="Storage Capacity"
value={value}
min={min}
step={1}
max={max}
defaultValue={defaultValue}
activeOption="unit"
onClick={() => { }}
onChange={handleCapacityChange}
/>
}
{type === 'spawn' &&
<>
<InputWithDropDown
label="Spawn Capacity"
value={value}
min={min}
step={1}
max={max}
defaultValue={defaultValue}
activeOption="unit"
onClick={() => { }}
onChange={handleCapacityChange}
/>
<LabledDropdown
label={"Material Type"}
defaultOption={currentMaterialType}
options={["Default material", "Material 1", "Material 2", "Material 3"]}
onSelect={handleMaterialTypeChange}
/>
</>
}
</>
);
};
export default StorageAction;
import React from "react";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
interface StorageActionProps {
type: "store" | "spawn" | "default";
value: string;
min: number;
max?: number;
defaultValue: string;
currentMaterialType: string;
handleCapacityChange: (value: string) => void;
handleMaterialTypeChange: (value: string) => void;
}
const StorageAction: React.FC<StorageActionProps> = ({ type, value, min, max, defaultValue, currentMaterialType, handleCapacityChange, handleMaterialTypeChange }) => {
return (
<>
{type === 'store' &&
<InputWithDropDown
label="Storage Capacity"
value={value}
min={min}
step={1}
max={max}
defaultValue={defaultValue}
activeOption="unit"
onClick={() => { }}
onChange={handleCapacityChange}
/>
}
{type === 'spawn' &&
<>
<InputWithDropDown
label="Spawn Capacity"
value={value}
min={min}
step={1}
max={max}
defaultValue={defaultValue}
activeOption="unit"
onClick={() => { }}
onChange={handleCapacityChange}
/>
<LabledDropdown
label={"Material Type"}
defaultOption={currentMaterialType}
options={["Default material", "Material 1", "Material 2", "Material 3"]}
onSelect={handleMaterialTypeChange}
/>
</>
}
</>
);
};
export default StorageAction;

View File

@@ -1,25 +1,25 @@
import React from "react";
import PreviewSelectionWithUpload from "../../../../../ui/inputs/PreviewSelectionWithUpload";
interface SwapActionProps {
onSelect: (option: string) => void;
defaultOption: string;
options: string[];
}
const SwapAction: React.FC<SwapActionProps> = ({
onSelect,
defaultOption,
options,
}) => {
return (
<PreviewSelectionWithUpload
label="Presets"
defaultOption={defaultOption}
options={options}
onSelect={onSelect}
/>
);
};
export default SwapAction;
import React from "react";
import PreviewSelectionWithUpload from "../../../../../ui/inputs/PreviewSelectionWithUpload";
interface SwapActionProps {
onSelect: (option: string) => void;
defaultOption: string;
options: string[];
}
const SwapAction: React.FC<SwapActionProps> = ({
onSelect,
defaultOption,
options,
}) => {
return (
<PreviewSelectionWithUpload
label="Presets"
defaultOption={defaultOption}
options={options}
onSelect={onSelect}
/>
);
};
export default SwapAction;

View File

@@ -1,70 +1,70 @@
import React from "react";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
interface TravelActionProps {
loadCapacity: {
value: string;
min: number;
max: number;
defaultValue: string;
onChange: (value: string) => void;
};
unloadDuration: {
value: string;
min: number;
max: number;
defaultValue: string;
onChange: (value: string) => void;
};
clearPoints: () => void;
}
const TravelAction: React.FC<TravelActionProps> = ({
loadCapacity,
unloadDuration,
clearPoints,
}) => {
return (
<>
<InputWithDropDown
label="Load Capacity"
value={loadCapacity.value}
min={loadCapacity.min}
max={loadCapacity.max}
defaultValue={loadCapacity.defaultValue}
step={1}
activeOption="unit"
onClick={() => { }}
onChange={loadCapacity.onChange}
/>
<InputWithDropDown
label="Unload Duration"
value={unloadDuration.value}
min={unloadDuration.min}
max={unloadDuration.max}
defaultValue={unloadDuration.defaultValue}
step={0.1}
activeOption="s"
onClick={() => { }}
onChange={unloadDuration.onChange}
/>
<div className="selected-actions-list">
<div className="value-field-container">
<div className="label">Reset</div>
<button
id="rest-button"
type="button"
className="regularDropdown-container"
onClick={() => {
clearPoints();
}}
>
Clear
</button>
</div>
</div>
</>
);
};
export default TravelAction;
import React from "react";
import InputWithDropDown from "../../../../../ui/inputs/InputWithDropDown";
interface TravelActionProps {
loadCapacity: {
value: string;
min: number;
max: number;
defaultValue: string;
onChange: (value: string) => void;
};
unloadDuration: {
value: string;
min: number;
max: number;
defaultValue: string;
onChange: (value: string) => void;
};
clearPoints: () => void;
}
const TravelAction: React.FC<TravelActionProps> = ({
loadCapacity,
unloadDuration,
clearPoints,
}) => {
return (
<>
<InputWithDropDown
label="Load Capacity"
value={loadCapacity.value}
min={loadCapacity.min}
max={loadCapacity.max}
defaultValue={loadCapacity.defaultValue}
step={1}
activeOption="unit"
onClick={() => { }}
onChange={loadCapacity.onChange}
/>
<InputWithDropDown
label="Unload Duration"
value={unloadDuration.value}
min={unloadDuration.min}
max={unloadDuration.max}
defaultValue={unloadDuration.defaultValue}
step={0.1}
activeOption="s"
onClick={() => { }}
onChange={unloadDuration.onChange}
/>
<div className="selected-actions-list">
<div className="value-field-container">
<div className="label">Reset</div>
<button
id="rest-button"
type="button"
className="regularDropdown-container"
onClick={() => {
clearPoints();
}}
>
Clear
</button>
</div>
</div>
</>
);
};
export default TravelAction;

View File

@@ -0,0 +1,210 @@
import { useEffect, useState } from "react";
import { MathUtils } from "three";
import RenameInput from "../../../../../ui/inputs/RenameInput";
import LabledDropdown from "../../../../../ui/inputs/LabledDropdown";
import Trigger from "../trigger/Trigger";
import { useSelectedEventData, useSelectedAction } from "../../../../../../store/simulation/useSimulationStore";
import ActionsList from "../components/ActionsList";
import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi";
import { useProductContext } from "../../../../../../modules/simulation/products/productContext";
import { useParams } from "react-router-dom";
import { useVersionContext } from "../../../../../../modules/builder/version/versionContext";
import { useSceneContext } from "../../../../../../modules/scene/sceneContext";
function CraneMechanics() {
const [activeOption, setActiveOption] = useState<"pickAndDrop">("pickAndDrop");
const [selectedPointData, setSelectedPointData] = useState<CranePointSchema | undefined>();
const { selectedEventData } = useSelectedEventData();
const { productStore } = useSceneContext();
const { getPointByUuid, updateAction, addAction, removeAction } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { selectedAction, setSelectedAction, clearSelectedAction } = useSelectedAction();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
useEffect(() => {
if (selectedEventData) {
const point = getPointByUuid(
selectedProduct.productUuid,
selectedEventData.data.modelUuid,
selectedEventData.selectedPoint
) as CranePointSchema | undefined;
if (point?.actions) {
setSelectedPointData(point);
if (point.actions.length > 0) {
const firstAction = point.actions[0];
setActiveOption(firstAction.actionType);
setSelectedAction(firstAction.actionUuid, firstAction.actionName);
}
}
} else {
clearSelectedAction();
}
}, [selectedEventData, selectedProduct]);
const updateBackend = (
productName: string,
productUuid: string,
projectId: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName,
productUuid,
projectId,
eventDatas: eventData,
versionId: selectedVersion?.versionId || "",
});
};
const handleRenameAction = (newName: string) => {
if (!selectedAction.actionId || !selectedPointData) return;
const event = updateAction(
selectedProduct.productUuid,
selectedAction.actionId,
{ actionName: newName }
);
const updatedActions = selectedPointData.actions.map(action =>
action.actionUuid === selectedAction.actionId
? { ...action, actionName: newName }
: action
);
setSelectedPointData({
...selectedPointData,
actions: updatedActions,
});
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
}
};
const handleAddAction = () => {
if (!selectedEventData || !selectedPointData) return;
const newAction = {
actionUuid: MathUtils.generateUUID(),
actionName: `Action ${selectedPointData.actions.length + 1}`,
actionType: "pickAndDrop" as const,
maxPickUpCount: 1,
triggers: [] as TriggerSchema[],
};
const event = addAction(
selectedProduct.productUuid,
selectedEventData.data.modelUuid,
selectedEventData.selectedPoint,
newAction
);
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
}
setSelectedPointData({
...selectedPointData,
actions: [...selectedPointData.actions, newAction],
});
setSelectedAction(newAction.actionUuid, newAction.actionName);
};
const handleDeleteAction = (actionUuid: string) => {
if (!selectedPointData) return;
const event = removeAction(
selectedProduct.productUuid,
actionUuid
);
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
}
const index = selectedPointData.actions.findIndex(a => a.actionUuid === actionUuid);
const newActions = selectedPointData.actions.filter(a => a.actionUuid !== actionUuid);
setSelectedPointData({
...selectedPointData,
actions: newActions,
});
if (selectedAction.actionId === actionUuid) {
const nextAction = newActions[index] || newActions[index - 1];
if (nextAction) {
setSelectedAction(nextAction.actionUuid, nextAction.actionName);
} else {
clearSelectedAction();
}
}
};
const availableActions = {
defaultOption: "pickAndDrop",
options: ["pickAndDrop"],
};
const currentAction = selectedPointData?.actions.find(a => a.actionUuid === selectedAction.actionId);
return (
<>
<section>
<ActionsList
selectedPointData={selectedPointData}
multipleAction
handleAddAction={handleAddAction}
handleDeleteAction={handleDeleteAction}
/>
{selectedAction.actionId && currentAction && (
<div className="selected-actions-details">
<div className="selected-actions-header">
<RenameInput
value={selectedAction.actionName || ""}
canEdit={false}
/>
</div>
<div className="selected-actions-list">
<LabledDropdown
defaultOption={activeOption}
options={availableActions.options}
onSelect={() => { }}
disabled={true}
/>
</div>
<div className="tirgger">
<Trigger
selectedPointData={selectedPointData as any}
type={"RoboticArm"}
/>
</div>
</div>
)}
</section>
</>
);
}
export default CraneMechanics

View File

@@ -17,7 +17,7 @@ import { useSceneContext } from "../../../../../../modules/scene/sceneContext";
import { useParams } from "react-router-dom";
function HumanMechanics() {
const [activeOption, setActiveOption] = useState<"worker" | "assembly">("worker");
const [activeOption, setActiveOption] = useState<"worker" | "assembly" | "operator">("worker");
const [speed, setSpeed] = useState("0.5");
const [loadCount, setLoadCount] = useState(0);
const [assemblyCount, setAssemblyCount] = useState(0);
@@ -78,7 +78,7 @@ function HumanMechanics() {
const newCurrentAction = getActionByUuid(selectedProduct.productUuid, actionUuid);
if (newCurrentAction && (newCurrentAction.actionType === 'assembly' || newCurrentAction?.actionType === 'worker')) {
if (newCurrentAction && (newCurrentAction.actionType === 'assembly' || newCurrentAction?.actionType === 'worker' || newCurrentAction?.actionType === "operator")) {
if (!selectedAction.actionId) {
setSelectedAction(newCurrentAction.actionUuid, newCurrentAction.actionName);
}
@@ -117,7 +117,7 @@ function HumanMechanics() {
const handleSelectActionType = (actionType: string) => {
if (!selectedAction.actionId || !currentAction || !selectedPointData) return;
const updatedAction = { ...currentAction, actionType: actionType as "worker" | "assembly" };
const updatedAction = { ...currentAction, actionType: actionType as "worker" | "assembly" | "operator" };
const updatedActions = selectedPointData.actions.map(action => action.actionUuid === updatedAction.actionUuid ? updatedAction : action);
const updatedPoint = { ...selectedPointData, actions: updatedActions };
@@ -397,12 +397,12 @@ function HumanMechanics() {
<LabledDropdown
label="Action Type"
defaultOption={activeOption}
options={["worker", "assembly"]}
options={["worker", "assembly", "operator"]}
onSelect={handleSelectActionType}
disabled={false}
/>
</div>
{currentAction.actionType === 'worker' &&
{(currentAction.actionType === 'worker' || currentAction.actionType === "operator") &&
<WorkerAction
loadCapacity={{
value: loadCapacity,

View File

@@ -10,100 +10,101 @@ import { updateProject } from "../../services/dashboard/updateProject";
import { getUserData } from "../../functions/getUserData";
const FileMenu: React.FC = () => {
const [openMenu, setOpenMenu] = useState(false);
const containerRef = useRef<HTMLButtonElement>(null);
let clickTimeout: NodeJS.Timeout | null = null;
const { projectName, setProjectName } = useProjectName();
const { dashBoardSocket } = useSocketStore();
const { projectId } = useParams();
const { userId, organization, email } = getUserData();
const [openMenu, setOpenMenu] = useState(false);
const containerRef = useRef<HTMLButtonElement>(null);
let clickTimeout: NodeJS.Timeout | null = null;
const { projectName, setProjectName } = useProjectName();
const { dashBoardSocket } = useSocketStore();
const { projectId } = useParams();
const { userId, organization, email } = getUserData();
const handleClick = () => {
if (clickTimeout) return;
setOpenMenu((prev) => !prev);
clickTimeout = setTimeout(() => {
clickTimeout = null;
}, 800);
const handleClick = () => {
if (clickTimeout) return;
setOpenMenu((prev) => !prev);
clickTimeout = setTimeout(() => {
clickTimeout = null;
}, 800);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setOpenMenu(false);
}
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setOpenMenu(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleProjectRename = async (projectName: string) => {
setProjectName(projectName);
if (!projectId) return;
const handleProjectRename = async (projectName: string) => {
setProjectName(projectName);
if (!projectId) return
// localStorage.setItem("projectName", newName);
// localStorage.setItem("projectName", newName);
try {
if (!email || !userId) return;
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
);
if (!email || !userId) return;
const updateProjects = {
projectId: projectUuid?._id,
organization,
userId,
projectName,
thumbnail: undefined,
};
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)
// if (dashBoardSocket) {
// const handleResponse = (data: any) => {
// console.log('Project update response:', data);
// dashBoardSocket.off("v1-project:response:update", handleResponse); // Clean up
// };
// dashBoardSocket.on("v1-project:response:update", handleResponse);
// dashBoardSocket.emit("v1:project:update", updateProjects);
// }
const updateProjects = {
projectId: projectUuid,
organization,
userId,
projectName,
thumbnail: undefined
}
//API for projects rename
// if (dashBoardSocket) {
// const handleResponse = (data: any) => {
// console.log('Project update response:', data);
// dashBoardSocket.off("v1-project:response:update", handleResponse); // Clean up
// };
// dashBoardSocket.on("v1-project:response:update", handleResponse);
// dashBoardSocket.emit("v1:project:update", updateProjects);
// }
//API for projects rename
const updatedProjectName = await updateProject(
projectId,
userId,
organization,
undefined,
projectName
);
//
} catch (error) {
console.error("Error updating project name:", error);
}
};
return (
<button
id="project-dropdown-button"
className="project-dropdowm-container"
ref={containerRef}
onClick={handleClick}
>
<div className="project-name">
<div className="icon">
<ProjectIcon />
</div>
<RenameInput value={projectName} onRename={handleProjectRename} />
</div>
<div className="more-options-button">
<ArrowIcon />
{openMenu && <MenuBar setOpenMenu={setOpenMenu} />}
</div>
</button>
);
const updatedProjectName = await updateProject(
projectId,
userId,
organization,
undefined,
projectName
);
} catch (error) {
console.error("Error updating project name:", error);
}
};
return (
<button
id="project-dropdown-button"
className="project-dropdowm-container"
ref={containerRef}
onClick={handleClick}
>
<div className="project-name">
<div className="icon">
<ProjectIcon />
</div>
<RenameInput value={projectName} onRename={handleProjectRename} />
</div>
<div className="more-options-button">
<ArrowIcon />
{openMenu && <MenuBar setOpenMenu={setOpenMenu} />}
</div>
</button>
);
};
export default FileMenu;

View File

@@ -1,30 +1,18 @@
import React, { useEffect, useState } from "react";
import React from "react";
import List from "./List";
import { AddIcon, ArrowIcon, FocusIcon } from "../../icons/ExportCommonIcons";
import KebabMenuListMultiSelect from "./KebebMenuListMultiSelect";
import { useSceneContext } from "../../../modules/scene/sceneContext";
interface DropDownListProps {
value?: string; // Value to display in the DropDownList
items?: { id: string; name: string }[]; // Items to display in the dropdown list
showFocusIcon?: boolean; // Determines if the FocusIcon should be displayed
showAddIcon?: boolean; // Determines if the AddIcon should be displayed
showKebabMenu?: boolean; // Determines if the KebabMenuList should be displayed
kebabMenuItems?: { id: string; name: string }[]; // Items for the KebabMenuList
defaultOpen?: boolean; // Determines if the dropdown list should be open by default
listType?: string; // Type of list to display
value?: string;
items?: { id: string; name: string }[];
showFocusIcon?: boolean;
showAddIcon?: boolean;
showKebabMenu?: boolean;
kebabMenuItems?: { id: string; name: string }[];
remove?: boolean;
}
interface Zone {
zoneUuid: string;
zoneName: string;
points: [number, number, number][]; // polygon vertices
}
interface ZoneData {
id: string;
name: string;
assets: { id: string; name: string; position?: []; rotation?: {} }[];
isOpen: boolean;
onToggle: () => void;
}
const DropDownList: React.FC<DropDownListProps> = ({
@@ -38,76 +26,13 @@ const DropDownList: React.FC<DropDownListProps> = ({
{ id: "Paths", name: "Paths" },
{ id: "Zones", name: "Zones" },
],
defaultOpen = false,
listType = "default",
remove,
isOpen,
onToggle,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(defaultOpen);
const handleToggle = () => {
setIsOpen((prev) => !prev); // Toggle the state
};
const [zoneDataList, setZoneDataList] = useState<ZoneData[]>([]);
const { assetStore, zoneStore } = useSceneContext();
const { assets } = assetStore();
const { zones } = zoneStore()
const isPointInsidePolygon = (
point: [number, number],
polygon: [number, number][]
) => {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0],
zi = polygon[i][1];
const xj = polygon[j][0],
zj = polygon[j][1];
const intersect =
// eslint-disable-next-line no-mixed-operators
zi > point[1] !== zj > point[1] &&
point[0] < ((xj - xi) * (point[1] - zi)) / (zj - zi + 0.000001) + xi;
if (intersect) inside = !inside;
}
return inside;
};
useEffect(() => {
const updatedZoneList: ZoneData[] = zones?.map((zone: any) => {
const polygon2D = zone.points.map((p: [number, number, number]) => [
p[0],
p[2],
]);
const assetsInZone = assets
.filter((item: any) => {
const [x, , z] = item.position;
return isPointInsidePolygon([x, z], polygon2D as [number, number][]);
})
.map((item: any) => ({
id: item.modelUuid,
name: item.modelName,
position: item.position,
rotation: item.rotation,
}));
return {
id: zone.zoneUuid,
name: zone.zoneName,
assets: assetsInZone,
};
});
setZoneDataList(updatedZoneList);
}, [zones, assets]);
return (
<div className="dropdown-list-container">
{/* eslint-disable-next-line */}
<div className="head" onClick={handleToggle}>
<div className="head" onClick={onToggle}>
<div className="value">{value}</div>
<div className="options">
{showFocusIcon && (
@@ -130,31 +55,15 @@ const DropDownList: React.FC<DropDownListProps> = ({
title="collapse-btn"
className="collapse-icon option"
style={{ transform: isOpen ? "rotate(0deg)" : "rotate(-90deg)" }}
// onClick={handleToggle}
>
<ArrowIcon />
</button>
</div>
</div>
{isOpen && (
<div className="lists-container">
{listType === "default" && <List items={items} remove={remove} />}
{listType === "outline" && (
<>
<DropDownList
value="Buildings"
showKebabMenu={false}
showAddIcon={false}
// items={zoneDataList}
/>
<DropDownList
value="Zones"
showKebabMenu={false}
showAddIcon={false}
items={zoneDataList}
/>
</>
)}
<List items={items} remove={remove} />
</div>
)}
</div>

View File

@@ -136,7 +136,6 @@ const List: React.FC<ListProps> = ({ items = [], remove }) => {
}
async function handleZoneAssetName(newName: string) {
if (zoneAssetId?.id) {
let response = await setAssetsApi({
modelUuid: zoneAssetId.id,

View File

@@ -0,0 +1,187 @@
import React from "react";
import { ArrayIcon, CopyIcon, DeleteIcon, DublicateIcon, FlipXAxisIcon, FlipZAxisIcon, FocusIcon, GroupIcon, ModifiersIcon, MoveIcon, PasteIcon, RenameIcon, RotateIcon, SubMenuIcon, TransformIcon } from "../../icons/ContextMenuIcons";
type ContextMenuProps = {
visibility: {
rename: boolean;
focus: boolean;
flipX: boolean;
flipZ: boolean;
move: boolean;
rotate: boolean;
duplicate: boolean;
copy: boolean;
paste: boolean;
modifier: boolean;
group: boolean;
array: boolean;
delete: boolean;
};
onRename: () => void;
onFocus: () => void;
onFlipX: () => void;
onFlipZ: () => void;
onMove: () => void;
onRotate: () => void;
onDuplicate: () => void;
onCopy: () => void;
onPaste: () => void;
onGroup: () => void;
onArray: () => void;
onDelete: () => void;
};
const ContextMenu: React.FC<ContextMenuProps> = ({
visibility,
onRename,
onFocus,
onFlipX,
onFlipZ,
onMove,
onRotate,
onDuplicate,
onCopy,
onPaste,
onGroup,
onArray,
onDelete,
}) => {
return (
<div className="context-menu">
{visibility.rename && (
<div className="menuItem">
<button className="button" onClick={onRename}>
<div className="icon"><RenameIcon /></div>
<span>Rename</span>
</button>
<span className="shortcut">F2</span>
</div>
)}
{visibility.focus && (
<div className="menuItem">
<button className="button" onClick={onFocus}>
<div className="icon"><FocusIcon /></div>
<span>Focus</span>
</button>
<span className="shortcut">F</span>
</div>
)}
{visibility.flipX && (
<div className="menuItem">
<button className="button" onClick={onFlipX}>
<div className="icon"><FlipXAxisIcon /></div>
<span>Flip to X axis</span>
</button>
</div>
)}
{visibility.flipZ && (
<div className="menuItem">
<button className="button" onClick={onFlipZ}>
<div className="icon"><FlipZAxisIcon /></div>
<span>Flip to Z axis</span>
</button>
</div>
)}
{(visibility.move || visibility.rotate) && (
<div className="menuItem">
<button className="button">
<div className="icon"><TransformIcon /></div>
<span>Transform</span>
</button>
<div className="more"><SubMenuIcon /></div>
<div className="submenu">
{visibility.move && (
<div className="menuItem">
<button className="button" onClick={onMove}>
<div className="icon"><MoveIcon /></div>
<span>Move</span>
</button>
<span className="shortcut">G</span>
</div>
)}
{visibility.rotate && (
<div className="menuItem">
<button className="button" onClick={onRotate}>
<div className="icon"><RotateIcon /></div>
<span>Rotate</span>
</button>
<span className="shortcut">R</span>
</div>
)}
</div>
</div>
)}
{visibility.duplicate && (
<div className="menuItem">
<button className="button" onClick={onDuplicate}>
<div className="icon"><DublicateIcon /></div>
<span>Duplicate</span>
</button>
<span className="shortcut">Ctrl + D</span>
</div>
)}
{visibility.copy && (
<div className="menuItem">
<button className="button" onClick={onCopy}>
<div className="icon"><CopyIcon /></div>
<span>Copy Objects</span>
</button>
<span className="shortcut">Ctrl + C</span>
</div>
)}
{visibility.paste && (
<div className="menuItem">
<button className="button" onClick={onPaste}>
<div className="icon"><PasteIcon /></div>
<span>Paste Objects</span>
</button>
<span className="shortcut">Ctrl + V</span>
</div>
)}
{visibility.modifier && (
<div className="menuItem">
<div className="icon"><ModifiersIcon /></div>
<button className="button">Modifiers</button>
</div>
)}
{(visibility.group || visibility.array) && (
<div className="menuItem">
<button className="button">Group / Array</button>
<div className="submenu">
{visibility.group && (
<div className="menuItem">
<button className="button" onClick={onGroup}>
<GroupIcon />
<span>Group</span>
</button>
<span className="shortcut">Ctrl + G</span>
</div>
)}
{visibility.array && (
<div className="menuItem">
<button className="button" onClick={onArray}>
<div className="icon"><ArrayIcon /></div>
<span>Array</span>
</button>
</div>
)}
</div>
</div>
)}
{visibility.delete && (
<div className="menuItem">
<button className="button" onClick={onDelete}>
<div className="icon"><DeleteIcon /></div>
<span>Delete</span>
</button>
<span className="shortcut">X</span>
</div>
)}
</div>
);
};
export default ContextMenu;

View File

@@ -0,0 +1,20 @@
export const isPointInsidePolygon = (
point: [number, number],
polygon: [number, number][]
) => {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0],
zi = polygon[i][1];
const xj = polygon[j][0],
zj = polygon[j][1];
const intersect =
// eslint-disable-next-line no-mixed-operators
zi > point[1] !== zj > point[1] &&
point[0] < ((xj - xi) * (point[1] - zi)) / (zj - zi + 0.000001) + xi;
if (intersect) inside = !inside;
}
return inside;
};

View File

@@ -119,6 +119,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "vehicle",
subType: item.eventData.subType as VehicleEventSchema['subType'] || '',
speed: 1,
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
@@ -151,6 +152,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "transfer",
subType: item.eventData.subType || '',
speed: 1,
points: item.eventData.points?.map((point: any, index: number) => ({
uuid: point.uuid || THREE.MathUtils.generateUUID(),
@@ -177,6 +179,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "machine",
subType: item.eventData.subType || '',
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
@@ -200,6 +203,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "roboticArm",
subType: item.eventData.subType || '',
speed: 1,
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
@@ -228,6 +232,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "storageUnit",
subType: item.eventData.subType || '',
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
@@ -250,6 +255,7 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "human",
subType: item.eventData.subType || '',
speed: 1,
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
@@ -270,6 +276,31 @@ function AssetsGroup({ plane }: { readonly plane: RefMesh }) {
}
}
addEvent(humanEvent);
} else if (item.eventData.type === 'Crane') {
const craneEvent: CraneEventSchema = {
modelUuid: item.modelUuid,
modelName: item.modelName,
position: item.position,
rotation: [item.rotation.x, item.rotation.y, item.rotation.z],
state: "idle",
type: "crane",
subType: item.eventData.subType as CraneEventSchema['subType'] || 'pillarJib',
point: {
uuid: item.eventData.point?.uuid || THREE.MathUtils.generateUUID(),
position: [item.eventData.point?.position[0] || 0, item.eventData.point?.position[1] || 0, item.eventData.point?.position[2] || 0],
rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0],
actions: [
{
actionUuid: THREE.MathUtils.generateUUID(),
actionName: "Action 1",
actionType: "pickAndDrop",
maxPickUpCount: 1,
triggers: []
}
]
}
}
addEvent(craneEvent);
}
} else {
assets.push({

View File

@@ -1,13 +1,13 @@
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
import * as Types from "../../../../types/world/worldTypes";
import { retrieveGLTF, storeGLTF } from "../../../../utils/indexDB/idbUtils";
// import { setAssetsApi } from '../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi';
import { Socket } from "socket.io-client";
import * as CONSTANTS from "../../../../types/world/worldConstants";
import PointsCalculator from "../../../simulation/events/points/functions/pointsCalculator";
import { getUserData } from "../../../../functions/getUserData";
// import { setAssetsApi } from '../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi';
async function addAssetModel(
scene: THREE.Scene,
@@ -165,7 +165,7 @@ async function handleModelLoad(
if (!data || !data.points) return;
const eventData: any = { type: selectedItem.type };
const eventData: any = { type: selectedItem.type, subType: selectedItem.subType };
if (selectedItem.type === "Conveyor") {
const ConveyorEvent: ConveyorEventSchema = {
@@ -175,6 +175,7 @@ async function handleModelLoad(
rotation: newFloorItem.rotation,
state: "idle",
type: "transfer",
subType: selectedItem.subType || '',
speed: 1,
points: data.points.map((point: THREE.Vector3, index: number) => {
const triggers: TriggerSchema[] = [];
@@ -243,6 +244,7 @@ async function handleModelLoad(
rotation: newFloorItem.rotation,
state: "idle",
type: "vehicle",
subType: selectedItem.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -280,6 +282,7 @@ async function handleModelLoad(
rotation: newFloorItem.rotation,
state: "idle",
type: "roboticArm",
subType: selectedItem.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -313,6 +316,7 @@ async function handleModelLoad(
rotation: newFloorItem.rotation,
state: "idle",
type: "machine",
subType: selectedItem.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [data.points[0].x, data.points[0].y, data.points[0].z],
@@ -341,6 +345,7 @@ async function handleModelLoad(
rotation: newFloorItem.rotation,
state: "idle",
type: "storageUnit",
subType: selectedItem.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [data.points[0].x, data.points[0].y, data.points[0].z],
@@ -368,6 +373,7 @@ async function handleModelLoad(
rotation: newFloorItem.rotation,
state: "idle",
type: "human",
subType: selectedItem.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -393,6 +399,36 @@ async function handleModelLoad(
position: humanEvent.point.position,
rotation: humanEvent.point.rotation,
}
} else if (selectedItem.type === "Crane") {
const craneEvent: CraneEventSchema = {
modelUuid: newFloorItem.modelUuid,
modelName: newFloorItem.modelName,
position: newFloorItem.position,
rotation: newFloorItem.rotation,
state: "idle",
type: "crane",
subType: selectedItem.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [data.points[0].x, data.points[0].y, data.points[0].z],
rotation: [0, 0, 0],
actions: [
{
actionUuid: THREE.MathUtils.generateUUID(),
actionName: "Action 1",
actionType: "pickAndDrop",
maxPickUpCount: 1,
triggers: []
}
]
}
}
addEvent(craneEvent);
eventData.point = {
uuid: craneEvent.point.uuid,
position: craneEvent.point.position,
rotation: craneEvent.point.rotation,
}
}
const completeData = {
@@ -401,11 +437,7 @@ async function handleModelLoad(
modelName: newFloorItem.modelName,
assetId: newFloorItem.assetId,
position: newFloorItem.position,
rotation: {
x: model.rotation.x,
y: model.rotation.y,
z: model.rotation.z,
},
rotation: { x: model.rotation.x, y: model.rotation.y, z: model.rotation.z, },
isLocked: false,
isVisible: true,
socketId: socket.id,
@@ -422,11 +454,7 @@ async function handleModelLoad(
modelName: completeData.modelName,
assetId: completeData.assetId,
position: completeData.position,
rotation: [
completeData.rotation.x,
completeData.rotation.y,
completeData.rotation.z,
] as [number, number, number],
rotation: [completeData.rotation.x, completeData.rotation.y, completeData.rotation.z,] as [number, number, number],
isLocked: completeData.isLocked,
isCollidable: false,
isVisible: completeData.isVisible,
@@ -442,11 +470,7 @@ async function handleModelLoad(
modelName: newFloorItem.modelName,
assetId: newFloorItem.assetId,
position: newFloorItem.position,
rotation: {
x: model.rotation.x,
y: model.rotation.y,
z: model.rotation.z,
},
rotation: { x: model.rotation.x, y: model.rotation.y, z: model.rotation.z, },
isLocked: false,
isVisible: true,
socketId: socket.id,
@@ -462,11 +486,7 @@ async function handleModelLoad(
modelName: data.modelName,
assetId: data.assetId,
position: data.position,
rotation: [data.rotation.x, data.rotation.y, data.rotation.z] as [
number,
number,
number
],
rotation: [data.rotation.x, data.rotation.y, data.rotation.z] as [number, number, number],
isLocked: data.isLocked,
isCollidable: false,
isVisible: data.isVisible,

View File

@@ -3,8 +3,15 @@ import { useMemo } from "react";
import { Cylinder } from "@react-three/drei";
export const AssetBoundingBox = ({ name, boundingBox, color, lineWidth, }: { name: string; boundingBox: Box3 | null; color: string; lineWidth: number; }) => {
const { edgeCylinders, center, size } = useMemo(() => {
if (!boundingBox) return { edgeCylinders: [], center: new Vector3(), size: new Vector3() };
const { edgeCylinders, cornerSpheres, center, size } = useMemo(() => {
if (!boundingBox) {
return {
edgeCylinders: [],
cornerSpheres: [],
center: new Vector3(),
size: new Vector3(),
};
}
const min = boundingBox.min;
const max = boundingBox.max;
@@ -50,7 +57,13 @@ export const AssetBoundingBox = ({ name, boundingBox, color, lineWidth, }: { nam
};
});
return { edgeCylinders, center, size };
const cornerSpheres = corners.map((corner, i) => ({
key: `corner-sphere-${i}`,
position: corner.clone(),
radius: radius * 1.5,
}));
return { edgeCylinders, cornerSpheres, center, size };
}, [boundingBox, lineWidth]);
if (!boundingBox) return null;
@@ -58,11 +71,18 @@ export const AssetBoundingBox = ({ name, boundingBox, color, lineWidth, }: { nam
return (
<group name={name}>
{edgeCylinders.map(({ key, position, rotation, length, radius }) => (
<Cylinder key={key} args={[radius, radius, length, 6]} position={position} quaternion={rotation} >
<Cylinder key={key} args={[radius, radius, length, 6]} position={position} quaternion={rotation}>
<meshBasicMaterial color={color} depthWrite={false} />
</Cylinder>
))}
{cornerSpheres.map(({ key, position, radius }) => (
<mesh key={key} position={position}>
<sphereGeometry args={[radius, 12, 12]} />
<meshBasicMaterial color={color} depthWrite={false} />
</mesh>
))}
<mesh visible={false} position={center}>
<boxGeometry args={[size.x, size.y, size.z]} />
</mesh>

View File

@@ -1,12 +1,9 @@
import * as THREE from 'three';
import { CameraControls } from '@react-three/drei';
import { ThreeEvent } from '@react-three/fiber';
import { useCallback } from 'react';
import { ProductStoreType } from '../../../../../../store/simulation/useProductStore';
import { EventStoreType } from '../../../../../../store/simulation/useEventsStore';
import { Socket } from 'socket.io-client';
import { ThreeEvent, useThree } from '@react-three/fiber';
import { useCallback, useEffect, useRef } from 'react';
import { useActiveTool, useToolMode } from '../../../../../../store/builder/store';
import { useActiveTool, useDeletableFloorItem, useSelectedFloorItem, useToggleView } from '../../../../../../store/builder/store';
import useModuleStore, { useSubModuleStore } from '../../../../../../store/useModuleStore';
import { useSocketStore } from '../../../../../../store/builder/store';
import { useSceneContext } from '../../../../../scene/sceneContext';
@@ -14,82 +11,104 @@ import { useProductContext } from '../../../../../simulation/products/productCon
import { useVersionContext } from '../../../../version/versionContext';
import { useParams } from 'react-router-dom';
import { getUserData } from '../../../../../../functions/getUserData';
import { useLeftData, useTopData } from '../../../../../../store/visualization/useZone3DWidgetStore';
import { useSelectedAsset } from '../../../../../../store/simulation/useSimulationStore';
import { upsertProductOrEventApi } from '../../../../../../services/simulation/products/UpsertProductOrEventApi';
// import { deleteFloorItem } from '../../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorItemApi';
export function useModelEventHandlers({
controls,
boundingBox,
groupRef,
toggleView,
deletableFloorItem,
setDeletableFloorItem,
setSelectedFloorItem,
gl,
setTop,
setLeft,
getIsEventInProduct,
getEventByModelUuid,
setSelectedAsset,
clearSelectedAsset,
removeAsset,
updateBackend,
leftDrag,
rightDrag
}: {
controls: CameraControls | any,
boundingBox: THREE.Box3 | null,
groupRef: React.RefObject<THREE.Group>,
toggleView: boolean,
deletableFloorItem: THREE.Object3D | null,
setDeletableFloorItem: (item: THREE.Object3D | null) => void,
setSelectedFloorItem: (item: THREE.Object3D | null) => void,
gl: THREE.WebGLRenderer,
setTop: (top: number) => void,
setLeft: (left: number) => void,
getIsEventInProduct: (productUuid: string, modelUuid: string) => boolean,
getEventByModelUuid: (modelUuid: string) => EventsSchema | undefined,
setSelectedAsset: (EventData: EventsSchema) => void,
clearSelectedAsset: () => void,
removeAsset: (modelUuid: string) => void,
updateBackend: (productName: string, productUuid: string, projectId: string, event: EventsSchema) => void,
leftDrag: React.MutableRefObject<boolean>,
rightDrag: React.MutableRefObject<boolean>
}) {
const { controls, gl, camera } = useThree();
const { activeTool } = useActiveTool();
const { activeModule } = useModuleStore();
const { toggleView } = useToggleView();
const { subModule } = useSubModuleStore();
const { socket } = useSocketStore();
const { eventStore, productStore } = useSceneContext();
const { eventStore, productStore, assetStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { removeAsset } = assetStore();
const { removeEvent, getEventByModelUuid } = eventStore();
const { getIsEventInProduct, addPoint, deleteEvent } = productStore();
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem();
const { setSelectedFloorItem } = useSelectedFloorItem();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { projectId } = useParams();
const { userId, organization } = getUserData();
const leftDrag = useRef(false);
const isLeftMouseDown = useRef(false);
const rightDrag = useRef(false);
const isRightMouseDown = useRef(false);
const { setTop } = useTopData();
const { setLeft } = useLeftData();
const updateBackend = (
productName: string,
productUuid: string,
projectId: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName: productName,
productUuid: productUuid,
projectId: projectId,
eventDatas: eventData,
versionId: selectedVersion?.versionId || '',
});
};
const handleDblClick = (asset: Asset) => {
if (asset && activeTool === "cursor" && boundingBox && groupRef.current && activeModule === 'builder') {
const size = boundingBox.getSize(new THREE.Vector3());
const center = boundingBox.getCenter(new THREE.Vector3());
const front = new THREE.Vector3(0, 0, 1);
groupRef.current.localToWorld(front);
front.sub(groupRef.current.position).normalize();
const frontView = false;
const distance = Math.max(size.x, size.y, size.z) * 2;
const newPosition = center.clone().addScaledVector(front, distance);
if (frontView) {
const size = boundingBox.getSize(new THREE.Vector3());
const center = boundingBox.getCenter(new THREE.Vector3());
(controls as CameraControls).setPosition(newPosition.x, newPosition.y, newPosition.z, true);
(controls as CameraControls).setTarget(center.x, center.y, center.z, true);
(controls as CameraControls).fitToBox(groupRef.current, true, {
cover: true,
paddingTop: 5,
paddingLeft: 5,
paddingBottom: 5,
paddingRight: 5,
});
const front = new THREE.Vector3(0, 0, 1);
groupRef.current.localToWorld(front);
front.sub(groupRef.current.position).normalize();
const distance = Math.max(size.x, size.y, size.z) * 2;
const newPosition = center.clone().addScaledVector(front, distance);
(controls as CameraControls).setPosition(newPosition.x, newPosition.y, newPosition.z, true);
(controls as CameraControls).setTarget(center.x, center.y, center.z, true);
(controls as CameraControls).fitToBox(groupRef.current, true, {
cover: true,
paddingTop: 5,
paddingLeft: 5,
paddingBottom: 5,
paddingRight: 5,
});
} else {
const collisionPos = new THREE.Vector3();
groupRef.current.getWorldPosition(collisionPos);
const currentPos = new THREE.Vector3().copy(camera.position);
const target = new THREE.Vector3();
if (!controls) return;
(controls as CameraControls).getTarget(target);
const direction = new THREE.Vector3().subVectors(target, currentPos).normalize();
const offsetDistance = 5;
const newCameraPos = new THREE.Vector3().copy(collisionPos).sub(direction.multiplyScalar(offsetDistance));
camera.position.copy(newCameraPos);
(controls as CameraControls).setLookAt(newCameraPos.x, newCameraPos.y, newCameraPos.z, collisionPos.x, 0, collisionPos.z, true);
}
setSelectedFloorItem(groupRef.current);
}
@@ -117,8 +136,8 @@ export function useModelEventHandlers({
const response = socket.emit('v1:model-asset:delete', data)
eventStore.getState().removeEvent(asset.modelUuid);
const updatedEvents = productStore.getState().deleteEvent(asset.modelUuid);
removeEvent(asset.modelUuid);
const updatedEvents = deleteEvent(asset.modelUuid);
updatedEvents.forEach((event) => {
updateBackend(
@@ -133,6 +152,21 @@ export function useModelEventHandlers({
removeAsset(asset.modelUuid);
push3D({
type: 'Scene',
actions: [
{
module: "builder",
actionType: "Asset-Delete",
asset: {
type: "Asset",
assetData: asset,
timeStap: new Date().toISOString()
}
}
]
});
echo.success("Model Removed!");
}
@@ -157,7 +191,7 @@ export function useModelEventHandlers({
}
}
const event = productStore.getState().addPoint(selectedProduct.productUuid, asset.modelUuid, conveyorPoint);
const event = addPoint(selectedProduct.productUuid, asset.modelUuid, conveyorPoint);
if (event) {
updateBackend(
@@ -169,7 +203,6 @@ export function useModelEventHandlers({
}
}
}
}
};
@@ -228,6 +261,50 @@ export function useModelEventHandlers({
}
}
useEffect(() => {
const canvasElement = gl.domElement;
const onPointerDown = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown.current = true;
leftDrag.current = false;
}
if (evt.button === 2) {
isRightMouseDown.current = true;
rightDrag.current = false;
}
};
const onPointerMove = () => {
if (isLeftMouseDown.current) {
leftDrag.current = true;
}
if (isRightMouseDown.current) {
rightDrag.current = true;
}
};
const onPointerUp = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown.current = false;
}
if (evt.button === 2) {
isRightMouseDown.current = false;
}
};
canvasElement.addEventListener('pointerdown', onPointerDown);
canvasElement.addEventListener('pointermove', onPointerMove);
canvasElement.addEventListener('pointerup', onPointerUp);
return () => {
canvasElement.removeEventListener('pointerdown', onPointerDown);
canvasElement.removeEventListener('pointermove', onPointerMove);
canvasElement.removeEventListener('pointerup', onPointerUp);
}
}, [gl])
return {
handleDblClick,
handleClick,

View File

@@ -1,154 +1,57 @@
import * as THREE from 'three';
import { SkeletonUtils } from 'three-stdlib';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { retrieveGLTF, storeGLTF } from '../../../../../utils/indexDB/idbUtils';
import { RapierRigidBody } from '@react-three/rapier';
import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import { ThreeEvent, useThree } from '@react-three/fiber';
import { useActiveTool, useDeletableFloorItem, useSelectedAssets, useSelectedFloorItem, useSocketStore, useToggleView, useToolMode } from '../../../../../store/builder/store';
import { useDeletableFloorItem, useSelectedAssets, useSelectedFloorItem, useToggleView, useToolMode } from '../../../../../store/builder/store';
import { AssetBoundingBox } from '../../functions/assetBoundingBox';
import { CameraControls } from '@react-three/drei';
import useModuleStore, { useSubModuleStore } from '../../../../../store/useModuleStore';
import { useLeftData, useTopData } from '../../../../../store/visualization/useZone3DWidgetStore';
import { useSelectedAsset } from '../../../../../store/simulation/useSimulationStore';
import { useProductContext } from '../../../../simulation/products/productContext';
import { useParams } from 'react-router-dom';
import { getUserData } from '../../../../../functions/getUserData';
import useModuleStore from '../../../../../store/useModuleStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useVersionContext } from '../../../version/versionContext';
import { upsertProductOrEventApi } from '../../../../../services/simulation/products/UpsertProductOrEventApi';
import { getAssetIksApi } from '../../../../../services/simulation/ik/getAssetIKs';
import { SkeletonUtils } from 'three-stdlib';
import { getAssetFieldApi } from '../../../../../services/factoryBuilder/asset/floorAsset/getAssetField';
import { ModelAnimator } from './animator/modelAnimator';
import ConveyorCollider from '../../../../scene/physics/conveyor/conveyorCollider';
import RibbonCollider from '../../../../scene/physics/conveyor/ribbonCollider';
import { getAssetConveyorPoints } from '../../../../../services/simulation/conveyorPoints/getAssetConveyorPoints';
import { useModelEventHandlers } from './eventHandlers/useEventHandlers';
function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendered: boolean, loader: GLTFLoader }) {
const url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`;
const { controls, gl } = useThree();
const savedTheme: string = localStorage.getItem("theme") || "light";
const { activeTool } = useActiveTool();
const { toolMode } = useToolMode();
const { toggleView } = useToggleView();
const { subModule } = useSubModuleStore();
const { activeModule } = useModuleStore();
const { assetStore, eventStore, productStore } = useSceneContext();
const { removeAsset, resetAnimation } = assetStore();
const { setTop } = useTopData();
const { setLeft } = useLeftData();
const { getIsEventInProduct, addPoint } = productStore();
const { getEventByModelUuid } = eventStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset();
const { socket } = useSocketStore();
const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem();
const { assetStore } = useSceneContext();
const { resetAnimation } = assetStore();
const { setDeletableFloorItem } = useDeletableFloorItem();
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
const leftDrag = useRef(false);
const isLeftMouseDown = useRef(false);
const rightDrag = useRef(false);
const isRightMouseDown = useRef(false);
const [gltfScene, setGltfScene] = useState<GLTF["scene"] | null>(null);
const [boundingBox, setBoundingBox] = useState<THREE.Box3 | null>(null);
const groupRef = useRef<THREE.Group>(null);
const rigidBodyRef = useRef<RapierRigidBody>(null);
const [isSelected, setIsSelected] = useState(false);
const [ikData, setIkData] = useState<any>();
const [ribbonData, setRibbonData] = useState<ConveyorPoints>();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
const groupRef = useRef<THREE.Group>(null);
const [fieldData, setFieldData] = useState<any>();
const { selectedAssets } = useSelectedAssets();
const updateBackend = (
productName: string,
productUuid: string,
projectId: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName: productName,
productUuid: productUuid,
projectId: projectId,
eventDatas: eventData,
versionId: selectedVersion?.versionId || '',
});
};
useEffect(() => {
if (!ikData && asset.eventData && asset.eventData.type === 'ArmBot') {
getAssetIksApi(asset.assetId).then((data) => {
if (data.iks) {
const iks: IK[] = data.iks;
setIkData(iks);
if (!fieldData && asset.eventData) {
getAssetFieldApi(asset.assetId).then((data) => {
if (data.type === 'ArmBot') {
if (data.data) {
const fieldData: IK[] = data.data;
setFieldData(fieldData);
}
} else if (data.type === 'Conveyor') {
if (data.data) {
const fieldData = data.data;
console.log('fieldData: ', fieldData);
setFieldData(fieldData);
}
} else if (data.type === 'Crane') {
if (data.data) {
const fieldData = data.data;
setFieldData(fieldData);
}
}
})
}
}, [asset.modelUuid, ikData])
useEffect(() => {
if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
getAssetConveyorPoints(asset.assetId).then((data) => {
if (data && data.conveyorPoints && data.conveyorPoints.type !== '') {
setRibbonData(data.conveyorPoints)
}
})
}
}, [asset.modelUuid, asset.eventData, ribbonData, boundingBox])
// useEffect(() => {
// if (!ribbonData && boundingBox && asset.eventData && asset.eventData.type === 'Conveyor') {
//
// if (asset.assetId === "97e037828ce57fa7bd1cc615") {
// setRibbonData({
// type: 'normal',
// points: [
// [
// [-2.4697049405553173e-9, 0.8729155659675598, -2.6850852955950217],
// [-2.4697049405553173e-9, 0.8729155659675598, 2.6950024154767225]
// ],
// [
// [-2.4697049405553173e-9, 1, -2.6850852955950217],
// [-2.4697049405553173e-9, 1, 2.6950024154767225]
// ]
// ]
// })
// }
// if (asset.assetId === "7ce992f669caaf2d384b9e76") {
// setRibbonData({
// type: 'curved',
// points: [
// [
// [-0.08963948491646367, 1.2324171172287208, 0.0013611617557632294],
// [2.745753362991343, 1.2324171172287208, -0.20188181291400256],
// [3.0696383388490056, 1.2324171172287208, -3.044220906761294],
// ],
// [
// [-0.08963948491646367, 2, 0.0013611617557632294],
// [2.745753362991343, 2, -0.20188181291400256],
// [3.0696383388490056, 2, -3.044220906761294],
// ]
// ],
// })
// }
// }
// }, [asset.modelUuid, asset.eventData, ribbonData, boundingBox])
useEffect(() => {
if (gltfScene) {
gltfScene.traverse((child: any) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
})
}
}, [gltfScene]);
}, [asset.modelUuid, fieldData])
useEffect(() => {
setDeletableFloorItem(null);
@@ -163,285 +66,6 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
}
}, [isRendered, selectedFloorItem])
useEffect(() => {
const loadModel = async () => {
try {
// Check Cache
const assetId = asset.assetId;
const cachedModel = THREE.Cache.get(assetId);
if (cachedModel) {
const clone: any = SkeletonUtils.clone(cachedModel.scene);
clone.animations = cachedModel.animations || [];
setGltfScene(clone);
calculateBoundingBox(clone);
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 ${asset.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) {
}
};
loader.load(modelUrl,
handleBackendLoad,
undefined,
(error) => {
echo.error(`[Backend] Error loading ${asset.modelName}:`);
}
);
} catch (err) {
}
};
const calculateBoundingBox = (scene: THREE.Object3D) => {
const box = new THREE.Box3().setFromObject(scene);
setBoundingBox(box);
};
loadModel();
}, []);
const handleDblClick = (asset: Asset) => {
if (asset) {
if (activeTool === "cursor" && boundingBox && groupRef.current && activeModule === 'builder') {
const size = boundingBox.getSize(new THREE.Vector3());
const center = boundingBox.getCenter(new THREE.Vector3());
const front = new THREE.Vector3(0, 0, 1);
groupRef.current.localToWorld(front);
front.sub(groupRef.current.position).normalize();
const distance = Math.max(size.x, size.y, size.z) * 2;
const newPosition = center.clone().addScaledVector(front, distance);
(controls as CameraControls).setPosition(
newPosition.x,
newPosition.y,
newPosition.z,
true
);
(controls as CameraControls).setTarget(center.x, center.y, center.z, true);
(controls as CameraControls).fitToBox(groupRef.current, true, {
cover: true,
paddingTop: 5,
paddingLeft: 5,
paddingBottom: 5,
paddingRight: 5,
});
setSelectedFloorItem(groupRef.current);
}
}
};
const handleClick = (evt: ThreeEvent<MouseEvent>, asset: Asset) => {
if (leftDrag.current || toggleView) return;
if (activeTool === 'delete' && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
//REST
// const response = await deleteFloorItem(organization, asset.modelUuid, asset.modelName);
//SOCKET
const data = {
organization,
modelUuid: asset.modelUuid,
modelName: asset.modelName,
socketId: socket.id,
userId,
versionId: selectedVersion?.versionId || '',
projectId
}
const response = socket.emit('v1:model-asset:delete', data)
eventStore.getState().removeEvent(asset.modelUuid);
const updatedEvents = productStore.getState().deleteEvent(asset.modelUuid);
updatedEvents.forEach((event) => {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
})
if (response) {
removeAsset(asset.modelUuid);
echo.success("Model Removed!");
}
} else if (activeModule === 'simulation' && subModule === "simulations" && activeTool === 'pen') {
if (asset.eventData && asset.eventData.type === 'Conveyor') {
const intersectedPoint = evt.point;
const localPosition = groupRef.current?.worldToLocal(intersectedPoint.clone());
if (localPosition) {
const conveyorPoint: ConveyorPointSchema = {
uuid: THREE.MathUtils.generateUUID(),
position: [localPosition?.x, localPosition?.y, localPosition?.z],
rotation: [0, 0, 0],
action: {
actionUuid: THREE.MathUtils.generateUUID(),
actionName: `Action 1`,
actionType: 'default',
material: 'Default Material',
delay: 0,
spawnInterval: 5,
spawnCount: 1,
triggers: []
}
}
const event = addPoint(selectedProduct.productUuid, asset.modelUuid, conveyorPoint);
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
}
}
}
}
}
const handlePointerOver = useCallback((asset: Asset) => {
if (activeTool === "delete" && activeModule === 'builder') {
if (deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
return;
} else {
setDeletableFloorItem(groupRef.current);
}
}
}, [activeTool, activeModule, deletableFloorItem]);
const handlePointerOut = useCallback((evt: ThreeEvent<MouseEvent>, asset: Asset) => {
if (evt.intersections.length === 0 && activeTool === "delete" && deletableFloorItem && deletableFloorItem.uuid === asset.modelUuid) {
setDeletableFloorItem(null);
}
}, [activeTool, deletableFloorItem]);
const handleContextMenu = (asset: Asset, evt: ThreeEvent<MouseEvent>) => {
if (rightDrag.current || toggleView) return;
if (activeTool === "cursor" && subModule === 'simulations') {
if (asset.modelUuid) {
const canvasElement = gl.domElement;
const isInProduct = getIsEventInProduct(selectedProduct.productUuid, asset.modelUuid);
if (isInProduct) {
const event = getEventByModelUuid(asset.modelUuid);
if (event) {
setSelectedAsset(event);
const canvasRect = canvasElement.getBoundingClientRect();
const relativeX = evt.clientX - canvasRect.left;
const relativeY = evt.clientY - canvasRect.top;
setTop(relativeY);
setLeft(relativeX);
} else {
clearSelectedAsset();
}
} else {
const event = getEventByModelUuid(asset.modelUuid);
if (event) {
setSelectedAsset(event)
const canvasRect = canvasElement.getBoundingClientRect();
const relativeX = evt.clientX - canvasRect.left;
const relativeY = evt.clientY - canvasRect.top;
setTop(relativeY);
setLeft(relativeX);
} else {
clearSelectedAsset()
}
}
} else {
clearSelectedAsset()
}
} else {
clearSelectedAsset()
}
}
useEffect(() => {
const canvasElement = gl.domElement;
const onPointerDown = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown.current = true;
leftDrag.current = false;
}
if (evt.button === 2) {
isRightMouseDown.current = true;
rightDrag.current = false;
}
};
const onPointerMove = () => {
if (isLeftMouseDown.current) {
leftDrag.current = true;
}
if (isRightMouseDown.current) {
rightDrag.current = true;
}
};
const onPointerUp = (evt: any) => {
if (evt.button === 0) {
isLeftMouseDown.current = false;
}
if (evt.button === 2) {
isRightMouseDown.current = false;
}
};
canvasElement.addEventListener('pointerdown', onPointerDown);
canvasElement.addEventListener('pointermove', onPointerMove);
canvasElement.addEventListener('pointerup', onPointerUp);
return () => {
canvasElement.removeEventListener('pointerdown', onPointerDown);
canvasElement.removeEventListener('pointermove', onPointerMove);
canvasElement.removeEventListener('pointerup', onPointerUp);
}
}, [gl])
useEffect(() => {
if (selectedAssets.length > 0) {
if (selectedAssets.some((selectedAsset: THREE.Object3D) => selectedAsset.userData.modelUuid === asset.modelUuid)) {
@@ -454,6 +78,89 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
}
}, [selectedAssets])
useEffect(() => {
if (gltfScene) {
gltfScene.traverse((child: any) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
})
}
}, [gltfScene]);
useEffect(() => {
// Calculate Bounding Box
const calculateBoundingBox = (scene: THREE.Object3D) => {
const box = new THREE.Box3().setFromObject(scene);
setBoundingBox(box);
};
// Check Cache
const assetId = asset.assetId;
const cachedModel = THREE.Cache.get(assetId);
if (cachedModel) {
const clone: any = SkeletonUtils.clone(cachedModel.scene);
clone.animations = cachedModel.animations || [];
setGltfScene(clone);
calculateBoundingBox(clone);
return;
}
// Check IndexedDB
retrieveGLTF(assetId).then((indexedDBModel) => {
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 ${asset.modelName}:`);
URL.revokeObjectURL(blobUrl);
}
);
return;
}
// Fetch from Backend
const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${assetId}`;
loader.load(
modelUrl,
(gltf: GLTF) => {
fetch(modelUrl)
.then((response) => response.blob())
.then((modelBlob) => storeGLTF(assetId, modelBlob))
.then(() => {
THREE.Cache.add(assetId, gltf);
setGltfScene(gltf.scene.clone());
calculateBoundingBox(gltf.scene);
})
.catch((error) => {
console.error(
`[Backend] Error storing/loading ${asset.modelName}:`,
error
);
});
},
undefined,
(error) => {
echo.error(`[Backend] Error loading ${asset.modelName}:`);
}
);
}).catch((err) => {
console.error("Failed to load model:", asset.assetId, err);
});
}, []);
const { handleDblClick, handleClick, handlePointerOver, handlePointerOut, handleContextMenu } = useModelEventHandlers({ boundingBox, groupRef });
return (
<group
key={asset.modelUuid}
@@ -463,7 +170,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
position={asset.position}
rotation={asset.rotation}
visible={asset.isVisible}
userData={{ ...asset, iks: ikData, rigidBodyRef: rigidBodyRef.current, boundingBox: boundingBox }}
userData={{ ...asset, fieldData: fieldData }}
castShadow
receiveShadow
onDoubleClick={(e) => {
@@ -506,7 +213,11 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
</>
) : (
<AssetBoundingBox name='Asset Fallback' boundingBox={boundingBox} color='gray' lineWidth={2.5} />
<>
{!isSelected &&
<AssetBoundingBox name='Asset Fallback' boundingBox={boundingBox} color='gray' lineWidth={2.5} />
}
</>
)}
{/* <ConveyorCollider
@@ -514,20 +225,19 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere
asset={asset}
/> */}
{ribbonData &&
{/* {ribbonData &&
<RibbonCollider
key={asset.modelUuid}
boundingBox={boundingBox}
ribbonData={ribbonData}
asset={asset}
/>
}
} */}
{isSelected &&
<AssetBoundingBox name='Asset BBox' boundingBox={boundingBox} color={savedTheme === "dark" ? "#c4abf1" : "#6f42c1"} lineWidth={2.7} />
}
</>
)}
</group>
)} </group>
);
}

View File

@@ -1,8 +1,8 @@
import { useEffect, useRef, useState } from "react";
import { useThree, useFrame } from "@react-three/fiber";
import { Vector3 } from "three";
import { Group, Vector3 } from "three";
import { CameraControls } from '@react-three/drei';
import { useLimitDistance, useRenderDistance, useSelectedFloorItem } from '../../../../store/builder/store';
import { useLimitDistance, useRenderDistance, useSelectedFloorItem, useToggleView } from '../../../../store/builder/store';
import { useSelectedAsset } from '../../../../store/simulation/useSimulationStore';
import { useSceneContext } from '../../../scene/sceneContext';
@@ -13,6 +13,7 @@ const distanceWorker = new Worker(new URL("../../../../services/factoryBuilder/w
function Models({ loader }: { loader: GLTFLoader }) {
const { controls, camera } = useThree();
const assetGroupRef = useRef<Group>(null);
const { assetStore } = useSceneContext();
const { assets } = assetStore();
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
@@ -45,6 +46,7 @@ function Models({ loader }: { loader: GLTFLoader }) {
return (
<group
name='Asset Group'
ref={assetGroupRef}
onPointerMissed={(e) => {
e.stopPropagation();
if (selectedFloorItem) {

View File

@@ -95,7 +95,7 @@ export default function Builder() {
<AssetsGroup plane={plane} />
{/* <mesh name='Walls-And-WallAssets-Group'>
<mesh name='Walls-And-WallAssets-Group'>
<Geometry ref={csgRef} useGroups>
<WallGroup />
@@ -103,7 +103,7 @@ export default function Builder() {
<WallAssetGroup />
</Geometry>
</mesh> */}
</mesh>
<AislesGroup />
@@ -113,9 +113,9 @@ export default function Builder() {
<MeasurementTool />
{/* <CalculateAreaGroup /> */}
<CalculateAreaGroup />
{/* <NavMesh /> */}
<NavMesh />
<DxfFile />

View File

@@ -140,7 +140,7 @@ function WallAssetInstance({ wallAsset }: { wallAsset: WallAsset }) {
const updatedWallAsset = updateWallAsset(wallAsset.modelUuid, {
wallUuid: intersect.object.userData.wallUuid,
position: [newPoint.x, wallAsset.wallAssetType === 'fixed-move' ? 0 : intersect.point.y, newPoint.z],
position: [newPoint.x, wallAsset.wallAssetType === 'fixedMove' ? 0 : intersect.point.y, newPoint.z],
rotation: [wallRotation.x, wallRotation.y, wallRotation.z],
});
@@ -190,7 +190,7 @@ function WallAssetInstance({ wallAsset }: { wallAsset: WallAsset }) {
updateWallAsset(wallAsset.modelUuid, {
wallUuid: intersect.object.userData.wallUuid,
position: [newPoint.x, wallAsset.wallAssetType === 'fixed-move' ? 0 : intersect.point.y, newPoint.z],
position: [newPoint.x, wallAsset.wallAssetType === 'fixedMove' ? 0 : intersect.point.y, newPoint.z],
rotation: [wallRotation.x, wallRotation.y, wallRotation.z],
});
}

View File

@@ -51,9 +51,9 @@ function WallAssetCreator() {
modelName: selectedItem.name,
modelUuid: MathUtils.generateUUID(),
wallUuid: wall.wallUuid,
wallAssetType: selectedItem.subCategory,
wallAssetType: selectedItem.subType,
assetId: selectedItem.id,
position: [closestPoint.x, selectedItem.subCategory === "fixed-move" ? 0 : intersect.point.y, closestPoint.z],
position: [closestPoint.x, selectedItem.subType === "fixedMove" ? 0 : intersect.point.y, closestPoint.z],
rotation: [wallRotation.x, wallRotation.y, wallRotation.z],
isLocked: false,
isVisible: true,

View File

@@ -0,0 +1,194 @@
import { useEffect, useState } from 'react';
import { useThree } from '@react-three/fiber';
import { CameraControls, Html, ScreenSpace } from '@react-three/drei';
import { useContextActionStore, useRenameModeStore, useSelectedAssets } from '../../../../store/builder/store';
import ContextMenu from '../../../../components/ui/menu/contextMenu';
function ContextControls() {
const { gl, controls } = useThree();
const [canRender, setCanRender] = useState(false);
const [visibility, setVisibility] = useState({ rename: true, focus: true, flipX: true, flipZ: true, move: true, rotate: true, duplicate: true, copy: true, paste: true, modifier: false, group: false, array: false, delete: true, });
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const { selectedAssets } = useSelectedAssets();
const { setContextAction } = useContextActionStore();
const { setIsRenameMode } = useRenameModeStore();
useEffect(() => {
if (selectedAssets.length === 1) {
setVisibility({
rename: true,
focus: true,
flipX: true,
flipZ: true,
move: true,
rotate: true,
duplicate: true,
copy: true,
paste: true,
modifier: false,
group: false,
array: false,
delete: true,
});
} else if (selectedAssets.length > 1) {
setVisibility({
rename: false,
focus: true,
flipX: true,
flipZ: true,
move: true,
rotate: true,
duplicate: true,
copy: true,
paste: true,
modifier: false,
group: true,
array: false,
delete: true,
});
} else {
setVisibility({
rename: false,
focus: false,
flipX: false,
flipZ: false,
move: false,
rotate: false,
duplicate: false,
copy: false,
paste: false,
modifier: false,
group: false,
array: false,
delete: false,
});
}
}, [selectedAssets]);
useEffect(() => {
const canvasElement = gl.domElement;
const handleContextClick = (event: MouseEvent) => {
event.preventDefault();
if (selectedAssets.length > 0) {
setMenuPosition({ x: event.clientX - gl.domElement.width / 2, y: event.clientY - gl.domElement.height / 2 });
setCanRender(true);
if (controls) {
(controls as CameraControls).enabled = false;
}
} else {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
}
};
if (selectedAssets.length > 0) {
canvasElement.addEventListener('contextmenu', handleContextClick)
} else {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setMenuPosition({ x: 0, y: 0 });
}
return () => {
canvasElement.removeEventListener('contextmenu', handleContextClick);
};
}, [gl, selectedAssets]);
const handleAssetRename = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("renameAsset");
setIsRenameMode(true);
}
const handleAssetFocus = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("focusAsset");
}
const handleAssetMove = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("moveAsset")
}
const handleAssetRotate = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("rotateAsset")
}
const handleAssetCopy = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("copyAsset")
}
const handleAssetPaste = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("pasteAsset")
}
const handleAssetDelete = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("deleteAsset")
}
const handleAssetDuplicate = () => {
setCanRender(false);
if (controls) {
(controls as CameraControls).enabled = true;
}
setContextAction("duplicateAsset")
}
return (
<>
{canRender && (
<ScreenSpace depth={1} >
<Html
style={{
position: 'fixed',
top: menuPosition.y,
left: menuPosition.x,
zIndex: 1000
}}
>
<ContextMenu
visibility={visibility}
onRename={() => handleAssetRename()}
onFocus={() => handleAssetFocus()}
onFlipX={() => console.log("Flip to X")}
onFlipZ={() => console.log("Flip to Z")}
onMove={() => handleAssetMove()}
onRotate={() => handleAssetRotate()}
onDuplicate={() => handleAssetDuplicate()}
onCopy={() => handleAssetCopy()}
onPaste={() => handleAssetPaste()}
onGroup={() => console.log("Group")}
onArray={() => console.log("Array")}
onDelete={() => handleAssetDelete()}
/>
</Html>
</ScreenSpace>
)}
</>
);
}
export default ContextControls;

View File

@@ -14,8 +14,10 @@ import TransformControl from "./transformControls/transformControls";
import { useParams } from "react-router-dom";
import { getUserData } from "../../../functions/getUserData";
import ContextControls from "./contextControls/contextControls";
import SelectionControls2D from "./selectionControls/selection2D/selectionControls2D";
import UndoRedo2DControls from "./undoRedoControls/undoRedo2D/undoRedo2DControls";
import UndoRedo3DControls from "./undoRedoControls/undoRedo3D/undoRedo3DControls";
export default function Controls() {
const controlsRef = useRef<CameraControls>(null);
@@ -144,8 +146,12 @@ export default function Controls() {
<UndoRedo2DControls />
<UndoRedo3DControls />
<TransformControl />
<ContextControls />
</>
);
}

View File

@@ -2,7 +2,7 @@ import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { SkeletonUtils } from "three-stdlib";
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { useParams } from "react-router-dom";
@@ -28,10 +28,11 @@ const CopyPasteControls3D = ({
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
const { socket } = useSocketStore();
const { assetStore, eventStore } = useSceneContext();
const { assetStore, eventStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { addEvent } = eventStore();
const { projectId } = useParams();
const { assets, addAsset, setPosition, updateAsset, removeAsset, getAssetById } = assetStore();
const { assets, addAsset, updateAsset, removeAsset, getAssetById } = assetStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { userId, organization } = getUserData();
@@ -39,10 +40,8 @@ const CopyPasteControls3D = ({
const [isPasting, setIsPasting] = useState(false);
const [relativePositions, setRelativePositions] = useState<THREE.Vector3[]>([]);
const [centerOffset, setCenterOffset] = useState<THREE.Vector3 | null>(null);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({
left: false,
right: false,
});
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const calculateRelativePositions = useCallback((objects: THREE.Object3D[]) => {
if (objects.length === 0) return { center: new THREE.Vector3(), relatives: [] };
@@ -60,6 +59,16 @@ const CopyPasteControls3D = ({
return { center, relatives };
}, []);
useEffect(() => {
if (contextAction === "copyAsset") {
setContextAction(null);
copySelection()
} else if (contextAction === "pasteAsset") {
setContextAction(null);
pasteCopiedObjects()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
const canvasElement = gl.domElement;
@@ -206,6 +215,9 @@ const CopyPasteControls3D = ({
const addPastedObjects = () => {
if (pastedObjects.length === 0) return;
const undoActions: UndoRedo3DAction[] = [];
const assetsToCopy: AssetData[] = [];
pastedObjects.forEach(async (pastedAsset: THREE.Object3D) => {
if (pastedAsset) {
const assetUuid = pastedAsset.userData.modelUuid;
@@ -232,6 +244,7 @@ const CopyPasteControls3D = ({
const eventData: any = {
type: pastedAsset.userData.eventData.type,
subType: pastedAsset.userData.eventData.subType,
};
if (pastedAsset.userData.eventData.type === "Conveyor") {
@@ -242,6 +255,7 @@ const CopyPasteControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: 'transfer',
subType: pastedAsset.userData.eventData.subType || '',
speed: 1,
points: updatedEventData.points.map((point: any, index: number) => ({
uuid: THREE.MathUtils.generateUUID(),
@@ -274,6 +288,7 @@ const CopyPasteControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "vehicle",
subType: pastedAsset.userData.eventData.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -312,6 +327,7 @@ const CopyPasteControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "roboticArm",
subType: pastedAsset.userData.eventData.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -346,6 +362,7 @@ const CopyPasteControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "machine",
subType: pastedAsset.userData.eventData.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
@@ -374,6 +391,7 @@ const CopyPasteControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "storageUnit",
subType: pastedAsset.userData.eventData.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
@@ -401,6 +419,7 @@ const CopyPasteControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "human",
subType: pastedAsset.userData.eventData.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -426,6 +445,36 @@ const CopyPasteControls3D = ({
position: humanEvent.point.position,
rotation: humanEvent.point.rotation
};
} else if (pastedAsset.userData.eventData.type === "Crane") {
const craneEvent: CraneEventSchema = {
modelUuid: newFloorItem.modelUuid,
modelName: newFloorItem.modelName,
position: newFloorItem.position,
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "crane",
subType: pastedAsset.userData.eventData.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
actions: [
{
actionUuid: THREE.MathUtils.generateUUID(),
actionName: "Action 1",
actionType: "pickAndDrop",
maxPickUpCount: 1,
triggers: []
}
]
}
}
addEvent(craneEvent);
eventData.point = {
uuid: craneEvent.point.uuid,
position: craneEvent.point.position,
rotation: craneEvent.point.rotation
};
}
newFloorItem.eventData = eventData;
@@ -500,9 +549,45 @@ const CopyPasteControls3D = ({
updateAsset(asset.modelUuid, asset);
}
assetsToCopy.push({
type: "Asset",
assetData: {
modelUuid: newFloorItem.modelUuid,
modelName: newFloorItem.modelName,
assetId: newFloorItem.assetId,
position: [position.x, position.y, position.z],
rotation: [pastedAsset.rotation.x, pastedAsset.rotation.y, pastedAsset.rotation.z],
isLocked: false,
isVisible: true,
isCollidable: false,
opacity: 1,
eventData: newFloorItem.eventData || undefined
},
timeStap: new Date().toISOString()
});
}
});
if (assetsToCopy.length === 1) {
undoActions.push({
module: "builder",
actionType: "Asset-Copied",
asset: assetsToCopy[0]
});
} else {
undoActions.push({
module: "builder",
actionType: "Assets-Copied",
assets: assetsToCopy
});
}
push3D({
type: 'Scene',
actions: undoActions
});
echo.success("Object added!");
clearSelection();
};

View File

@@ -2,13 +2,14 @@ import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { SkeletonUtils } from "three-stdlib";
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { useParams } from "react-router-dom";
import { getUserData } from "../../../../../functions/getUserData";
import { useSceneContext } from "../../../sceneContext";
import { useVersionContext } from "../../../../builder/version/versionContext";
import { handleAssetPositionSnap } from "./functions/handleAssetPositionSnap";
// import { setAssetsApi } from "../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
@@ -26,7 +27,8 @@ const DuplicationControls3D = ({
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
const { socket } = useSocketStore();
const { assetStore, eventStore } = useSceneContext();
const { assetStore, eventStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { addEvent } = eventStore();
const { projectId } = useParams();
const { assets, addAsset, updateAsset, removeAsset, getAssetById } = assetStore();
@@ -34,19 +36,29 @@ const DuplicationControls3D = ({
const { selectedVersion } = selectedVersionStore();
const { userId, organization } = getUserData();
const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">("");
const [axisConstraint, setAxisConstraint] = useState<"x" | "z" | null>(null);
const [dragOffset, setDragOffset] = useState<THREE.Vector3 | null>(null);
const [initialPositions, setInitialPositions] = useState<Record<string, THREE.Vector3>>({});
const [isDuplicating, setIsDuplicating] = useState(false);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({
left: false,
right: false,
});
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const fineMoveBaseRef = useRef<THREE.Vector3 | null>(null);
const lastPointerPositionRef = useRef<THREE.Vector3 | null>(null);
const wasShiftHeldRef = useRef(false);
const calculateDragOffset = useCallback((point: THREE.Object3D, hitPoint: THREE.Vector3) => {
const pointPosition = new THREE.Vector3().copy(point.position);
return new THREE.Vector3().subVectors(pointPosition, hitPoint);
}, []);
useEffect(() => {
if (contextAction === "duplicateAsset") {
setContextAction(null);
duplicateSelection()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
const canvasElement = gl.domElement;
@@ -79,11 +91,33 @@ const DuplicationControls3D = ({
removeAsset(obj.userData.modelUuid);
});
}
setKeyEvent("");
};
const onKeyDown = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (isDuplicating && duplicatedObjects.length > 0) {
if (event.key.toLowerCase() === 'x') {
setAxisConstraint(prev => prev === 'x' ? null : 'x');
event.preventDefault();
return;
}
if (event.key.toLowerCase() === 'z') {
setAxisConstraint(prev => prev === 'z' ? null : 'z');
event.preventDefault();
return;
}
}
if (keyCombination !== keyEvent) {
if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") {
setKeyEvent(keyCombination);
} else {
setKeyEvent("");
}
}
if (keyCombination === "Ctrl+D" && movedObjects.length === 0 && rotatedObjects.length === 0) {
duplicateSelection();
}
@@ -96,11 +130,34 @@ const DuplicationControls3D = ({
}
};
const onKeyUp = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (keyCombination === "") {
setKeyEvent("");
} else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") {
setKeyEvent(keyCombination);
}
if (duplicatedObjects[0] && keyEvent !== "" && keyCombination === '') {
const intersectionPoint = new THREE.Vector3();
raycaster.setFromCamera(pointer, camera);
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid);
if (model) {
const newOffset = calculateDragOffset(model, intersectionPoint);
setDragOffset(newOffset);
}
}
}
};
if (!toggleView) {
canvasElement.addEventListener("pointerdown", onPointerDown);
canvasElement.addEventListener("pointermove", onPointerMove);
canvasElement.addEventListener("pointerup", onPointerUp);
canvasElement.addEventListener("keydown", onKeyDown);
canvasElement.addEventListener("keyup", onKeyUp);
}
return () => {
@@ -108,8 +165,9 @@ const DuplicationControls3D = ({
canvasElement.removeEventListener("pointermove", onPointerMove);
canvasElement.removeEventListener("pointerup", onPointerUp);
canvasElement.removeEventListener("keydown", onKeyDown);
canvasElement.addEventListener("keyup", onKeyUp);
};
}, [assets, camera, controls, scene, toggleView, selectedAssets, duplicatedObjects, movedObjects, socket, rotatedObjects]);
}, [assets, camera, controls, scene, toggleView, selectedAssets, duplicatedObjects, movedObjects, socket, rotatedObjects, keyEvent]);
useFrame(() => {
if (!isDuplicating || duplicatedObjects.length === 0) return;
@@ -129,7 +187,9 @@ const DuplicationControls3D = ({
}
if (dragOffset) {
const adjustedHit = new THREE.Vector3().addVectors(intersectionPoint, dragOffset);
const rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset);
const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid);
const baseNewPosition = handleAssetPositionSnap({ rawBasePosition, intersectionPoint, model, axisConstraint, keyEvent, fineMoveBaseRef, lastPointerPositionRef, wasShiftHeldRef });
duplicatedObjects.forEach((duplicatedObject: THREE.Object3D) => {
if (duplicatedObject.userData.modelUuid) {
@@ -143,7 +203,7 @@ const DuplicationControls3D = ({
);
const model = scene.getObjectByProperty("uuid", duplicatedObject.userData.modelUuid);
const newPosition = new THREE.Vector3().addVectors(adjustedHit, relativeOffset);
const newPosition = new THREE.Vector3().addVectors(baseNewPosition, relativeOffset);
const positionArray: [number, number, number] = [newPosition.x, newPosition.y, newPosition.z];
if (model) {
@@ -156,6 +216,21 @@ const DuplicationControls3D = ({
}
});
useEffect(() => {
if (duplicatedObjects.length > 0) {
const intersectionPoint = new THREE.Vector3();
raycaster.setFromCamera(pointer, camera);
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const model = scene.getObjectByProperty("uuid", duplicatedObjects[0].userData.modelUuid);
if (model) {
const newOffset = calculateDragOffset(model, intersectionPoint);
setDragOffset(newOffset);
}
}
}
}, [axisConstraint, camera, duplicatedObjects])
const duplicateSelection = useCallback(() => {
if (selectedAssets.length > 0 && duplicatedObjects.length === 0) {
const positions: Record<string, THREE.Vector3> = {};
@@ -207,6 +282,9 @@ const DuplicationControls3D = ({
const addDuplicatedAssets = () => {
if (duplicatedObjects.length === 0) return;
const undoActions: UndoRedo3DAction[] = [];
const assetsToDuplicate: AssetData[] = [];
duplicatedObjects.forEach(async (duplicatedAsset: THREE.Object3D) => {
if (duplicatedAsset) {
const assetUuid = duplicatedAsset.userData.modelUuid;
@@ -233,6 +311,7 @@ const DuplicationControls3D = ({
const eventData: any = {
type: duplicatedAsset.userData.eventData.type,
subType: duplicatedAsset.userData.eventData.subType,
};
if (duplicatedAsset.userData.eventData.type === "Conveyor") {
@@ -243,6 +322,7 @@ const DuplicationControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: 'transfer',
subType: duplicatedAsset.userData.eventData.subType || '',
speed: 1,
points: updatedEventData.points.map((point: any, index: number) => ({
uuid: THREE.MathUtils.generateUUID(),
@@ -275,6 +355,7 @@ const DuplicationControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "vehicle",
subType: duplicatedAsset.userData.eventData.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -313,6 +394,7 @@ const DuplicationControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "roboticArm",
subType: duplicatedAsset.userData.eventData.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -347,6 +429,7 @@ const DuplicationControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "machine",
subType: duplicatedAsset.userData.eventData.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
@@ -375,6 +458,7 @@ const DuplicationControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "storageUnit",
subType: duplicatedAsset.userData.eventData.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
@@ -402,6 +486,7 @@ const DuplicationControls3D = ({
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "human",
subType: duplicatedAsset.userData.eventData.subType || '',
speed: 1,
point: {
uuid: THREE.MathUtils.generateUUID(),
@@ -427,6 +512,36 @@ const DuplicationControls3D = ({
position: humanEvent.point.position,
rotation: humanEvent.point.rotation
};
} else if (duplicatedAsset.userData.eventData.type === "Crane") {
const craneEvent: CraneEventSchema = {
modelUuid: newFloorItem.modelUuid,
modelName: newFloorItem.modelName,
position: newFloorItem.position,
rotation: [newFloorItem.rotation.x, newFloorItem.rotation.y, newFloorItem.rotation.z],
state: "idle",
type: "crane",
subType: duplicatedAsset.userData.eventData.subType || '',
point: {
uuid: THREE.MathUtils.generateUUID(),
position: [updatedEventData.point.position[0], updatedEventData.point.position[1], updatedEventData.point.position[2]],
rotation: [updatedEventData.point.rotation[0], updatedEventData.point.rotation[1], updatedEventData.point.rotation[2]],
actions: [
{
actionUuid: THREE.MathUtils.generateUUID(),
actionName: "Action 1",
actionType: "pickAndDrop",
maxPickUpCount: 1,
triggers: []
}
]
}
}
addEvent(craneEvent);
eventData.point = {
uuid: craneEvent.point.uuid,
position: craneEvent.point.position,
rotation: craneEvent.point.rotation
};
}
newFloorItem.eventData = eventData;
@@ -501,9 +616,45 @@ const DuplicationControls3D = ({
updateAsset(asset.modelUuid, asset);
}
assetsToDuplicate.push({
type: "Asset",
assetData: {
modelUuid: newFloorItem.modelUuid,
modelName: newFloorItem.modelName,
assetId: newFloorItem.assetId,
position: [position.x, position.y, position.z],
rotation: [duplicatedAsset.rotation.x, duplicatedAsset.rotation.y, duplicatedAsset.rotation.z],
isLocked: false,
isVisible: true,
isCollidable: false,
opacity: 1,
eventData: newFloorItem.eventData || undefined
},
timeStap: new Date().toISOString()
});
}
});
if (assetsToDuplicate.length === 1) {
undoActions.push({
module: "builder",
actionType: "Asset-Copied",
asset: assetsToDuplicate[0]
});
} else {
undoActions.push({
module: "builder",
actionType: "Assets-Copied",
assets: assetsToDuplicate
});
}
push3D({
type: 'Scene',
actions: undoActions
});
echo.success("Object duplicated!");
clearSelection();
};
@@ -516,6 +667,7 @@ const DuplicationControls3D = ({
setSelectedAssets([]);
setIsDuplicating(false);
setDragOffset(null);
setAxisConstraint(null);
};
return null;

View File

@@ -0,0 +1,75 @@
import * as THREE from "three";
export function handleAssetPositionSnap({
rawBasePosition,
intersectionPoint,
model,
axisConstraint,
keyEvent,
fineMoveBaseRef,
lastPointerPositionRef,
wasShiftHeldRef
}: {
rawBasePosition: THREE.Vector3;
intersectionPoint: THREE.Vector3;
model: THREE.Object3D | undefined;
axisConstraint: "x" | "z" | null;
keyEvent: string;
fineMoveBaseRef: React.MutableRefObject<THREE.Vector3 | null>;
lastPointerPositionRef: React.MutableRefObject<THREE.Vector3 | null>;
wasShiftHeldRef: React.MutableRefObject<boolean>;
}): THREE.Vector3 {
const CTRL_DISTANCE = 0.5;
const SHIFT_DISTANCE = 0.05;
const CTRL_SHIFT_DISTANCE = 0.05;
const isShiftHeld = keyEvent.includes("Shift");
// Handle Shift toggle state
if (isShiftHeld !== wasShiftHeldRef.current && model) {
if (isShiftHeld) {
fineMoveBaseRef.current = model.position.clone();
lastPointerPositionRef.current = intersectionPoint.clone();
} else {
fineMoveBaseRef.current = null;
}
wasShiftHeldRef.current = isShiftHeld;
}
// Start from raw
let baseNewPosition = rawBasePosition.clone();
// Apply snapping / fine move
if (keyEvent === "Ctrl") {
baseNewPosition.set(
Math.round(baseNewPosition.x / CTRL_DISTANCE) * CTRL_DISTANCE,
baseNewPosition.y,
Math.round(baseNewPosition.z / CTRL_DISTANCE) * CTRL_DISTANCE
);
} else if (keyEvent === "Ctrl+Shift") {
if (isShiftHeld && fineMoveBaseRef.current && lastPointerPositionRef.current) {
const pointerDelta = new THREE.Vector3().subVectors(intersectionPoint, lastPointerPositionRef.current);
baseNewPosition = fineMoveBaseRef.current.clone().add(pointerDelta.multiplyScalar(SHIFT_DISTANCE));
}
baseNewPosition.set(
Math.round(baseNewPosition.x / CTRL_SHIFT_DISTANCE) * CTRL_SHIFT_DISTANCE,
baseNewPosition.y,
Math.round(baseNewPosition.z / CTRL_SHIFT_DISTANCE) * CTRL_SHIFT_DISTANCE
);
} else if (isShiftHeld && fineMoveBaseRef.current && lastPointerPositionRef.current) {
const pointerDelta = new THREE.Vector3().subVectors(intersectionPoint, lastPointerPositionRef.current);
baseNewPosition = fineMoveBaseRef.current.clone().add(pointerDelta.multiplyScalar(SHIFT_DISTANCE));
}
// Apply axis constraint last
if (axisConstraint && model) {
const currentBasePosition = model.position.clone();
if (axisConstraint === 'x') {
baseNewPosition.set(baseNewPosition.x, currentBasePosition.y, currentBasePosition.z);
} else if (axisConstraint === 'z') {
baseNewPosition.set(currentBasePosition.x, currentBasePosition.y, baseNewPosition.z);
}
}
return baseNewPosition;
}

View File

@@ -0,0 +1,74 @@
import * as THREE from "three";
export function handleAssetRotationSnap({
object,
pointerDeltaX,
keyEvent,
snapBaseRef,
prevRotationRef,
wasShiftHeldRef
}: {
object: THREE.Object3D;
pointerDeltaX: number;
keyEvent: string;
snapBaseRef: React.MutableRefObject<number | null>;
prevRotationRef: React.MutableRefObject<number | null>;
wasShiftHeldRef: React.MutableRefObject<boolean>;
}): number {
const SHIFT_SPEED = 0.5; // Fine rotation speed
const NORMAL_SPEED = 5; // Normal rotation speed
const CTRL_SNAP_DEG = 15; // 15 degrees
const CTRL_SHIFT_SNAP_DEG = 5; // 5 degrees
const isShiftHeld = keyEvent.includes("Shift");
const isCtrlHeld = keyEvent.includes("Ctrl");
const speedFactor = isShiftHeld ? SHIFT_SPEED : NORMAL_SPEED;
let deltaAngle = pointerDeltaX * speedFactor;
// Track if modifier changed
const modifierChanged = isShiftHeld !== wasShiftHeldRef.current;
if (modifierChanged) {
wasShiftHeldRef.current = isShiftHeld;
if (isCtrlHeld) snapBaseRef.current = null;
}
if (isCtrlHeld) {
const snapDeg = isShiftHeld ? CTRL_SHIFT_SNAP_DEG : CTRL_SNAP_DEG;
const snapRad = THREE.MathUtils.degToRad(snapDeg);
// Get current Y rotation from object's quaternion
const euler = new THREE.Euler().setFromQuaternion(object.quaternion, 'YXZ');
let currentAngle = euler.y;
// Initialize snap base on first frame
if (snapBaseRef.current === null) {
snapBaseRef.current = currentAngle;
prevRotationRef.current = currentAngle;
}
// Accumulate the total rotation from the base
const totalRotation = snapBaseRef.current + deltaAngle;
// Snap to nearest increment
const snappedAngle = Math.round(totalRotation / snapRad) * snapRad;
// Calculate the delta needed to reach the snapped angle from current rotation
let targetDelta = snappedAngle - currentAngle;
// Handle wrapping around 360 degrees
if (Math.abs(targetDelta) > Math.PI) {
targetDelta = targetDelta - Math.sign(targetDelta) * 2 * Math.PI;
}
// Update snap base for next frame
snapBaseRef.current = totalRotation;
return targetDelta;
} else {
// Reset snapping when Ctrl is not held
snapBaseRef.current = null;
prevRotationRef.current = null;
return deltaAngle;
}
}

View File

@@ -1,11 +1,11 @@
import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { useSelectedAssets, useSocketStore, useToggleView, } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
import { snapControls } from "../../../../../utils/handleSnap";
import { handleAssetPositionSnap } from "./functions/handleAssetPositionSnap";
import DistanceFindingControls from "./distanceFindingControls";
import { useParams } from "react-router-dom";
import { useProductContext } from "../../../../simulation/products/productContext";
@@ -34,19 +34,25 @@ function MoveControls3D({
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { socket } = useSocketStore();
const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">("");
const { userId, organization } = getUserData();
const { projectId } = useParams();
const { assetStore, eventStore, productStore } = useSceneContext();
const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { updateAsset, getAssetById } = assetStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">("");
const [axisConstraint, setAxisConstraint] = useState<"x" | "z" | null>(null);
const [dragOffset, setDragOffset] = useState<THREE.Vector3 | null>(null);
const [initialPositions, setInitialPositions] = useState<Record<string, THREE.Vector3>>({});
const [initialStates, setInitialStates] = useState<Record<string, { position: THREE.Vector3; rotation?: THREE.Euler; }>>({});
const [isMoving, setIsMoving] = useState(false);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const fineMoveBaseRef = useRef<THREE.Vector3 | null>(null);
const lastPointerPositionRef = useRef<THREE.Vector3 | null>(null);
const wasShiftHeldRef = useRef(false);
const updateBackend = (
productName: string,
@@ -63,6 +69,13 @@ function MoveControls3D({
});
};
useEffect(() => {
if (contextAction === "moveAsset") {
setContextAction(null);
moveAssets()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
@@ -76,10 +89,21 @@ function MoveControls3D({
};
const onKeyUp = (event: KeyboardEvent) => {
const isModifierKey = (!event.shiftKey && !event.ctrlKey);
const keyCombination = detectModifierKeys(event);
if (isModifierKey && keyEvent !== "") {
if (keyCombination === "") {
setKeyEvent("");
} else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") {
setKeyEvent(keyCombination);
}
if (movedObjects[0]) {
const intersectionPoint = new THREE.Vector3();
raycaster.setFromCamera(pointer, camera);
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const newOffset = calculateDragOffset(movedObjects[0], intersectionPoint);
setDragOffset(newOffset);
}
}
};
@@ -104,7 +128,6 @@ function MoveControls3D({
clearSelection();
setMovedObjects([]);
}
setKeyEvent("");
};
const onKeyDown = (event: KeyboardEvent) => {
@@ -113,6 +136,19 @@ function MoveControls3D({
if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0)
return;
if (isMoving && movedObjects.length > 0) {
if (event.key.toLowerCase() === 'x') {
setAxisConstraint(prev => prev === 'x' ? null : 'x');
event.preventDefault();
return;
}
if (event.key.toLowerCase() === 'z') {
setAxisConstraint(prev => prev === 'z' ? null : 'z');
event.preventDefault();
return;
}
}
if (keyCombination !== keyEvent) {
if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") {
setKeyEvent(keyCombination);
@@ -183,9 +219,22 @@ function MoveControls3D({
}
}
});
}, 0)
setAxisConstraint(null);
}, 100)
}, [movedObjects, initialStates, updateAsset]);
useEffect(() => {
if (movedObjects.length > 0) {
const intersectionPoint = new THREE.Vector3();
raycaster.setFromCamera(pointer, camera);
const hit = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (hit) {
const newOffset = calculateDragOffset(movedObjects[0], intersectionPoint);
setDragOffset(newOffset);
}
}
}, [axisConstraint, camera, movedObjects])
useFrame(() => {
if (!isMoving || movedObjects.length === 0) return;
@@ -204,20 +253,8 @@ function MoveControls3D({
if (dragOffset) {
const rawBasePosition = new THREE.Vector3().addVectors(intersectionPoint, dragOffset);
let moveDistance = keyEvent.includes("Shift") ? 0.05 : 1;
const initialBasePosition = initialPositions[movedObjects[0].uuid];
const positionDifference = new THREE.Vector3().subVectors(rawBasePosition, initialBasePosition);
let adjustedDifference = positionDifference.multiplyScalar(moveDistance);
const baseNewPosition = new THREE.Vector3().addVectors(initialBasePosition, adjustedDifference);
if (keyEvent.includes("Ctrl")) {
baseNewPosition.x = snapControls(baseNewPosition.x, keyEvent);
baseNewPosition.z = snapControls(baseNewPosition.z, keyEvent);
}
const model = movedObjects[0];
const baseNewPosition = handleAssetPositionSnap({ rawBasePosition, intersectionPoint, model, axisConstraint, keyEvent, fineMoveBaseRef, lastPointerPositionRef, wasShiftHeldRef });
movedObjects.forEach((movedAsset: THREE.Object3D) => {
if (movedAsset.userData.modelUuid) {
@@ -284,6 +321,9 @@ function MoveControls3D({
const placeMovedAssets = () => {
if (movedObjects.length === 0) return;
const undoActions: UndoRedo3DAction[] = [];
const assetsToUpdate: AssetData[] = [];
movedObjects.forEach(async (movedAsset: THREE.Object3D) => {
if (movedAsset) {
const assetUuid = movedAsset.userData.modelUuid;
@@ -291,6 +331,32 @@ function MoveControls3D({
const model = scene.getObjectByProperty("uuid", movedAsset.userData.modelUuid);
if (!asset || !model) return;
const position = new THREE.Vector3().copy(model.position);
const initialState = initialStates[movedAsset.uuid];
if (initialState) {
assetsToUpdate.push({
type: "Asset",
assetData: {
...asset,
position: [
initialState.position.x,
initialState.position.y,
initialState.position.z
],
rotation: [
initialState.rotation?.x || 0,
initialState.rotation?.y || 0,
initialState.rotation?.z || 0
]
},
newData: {
...asset,
position: [position.x, position.y, position.z],
rotation: [movedAsset.rotation.x, movedAsset.rotation.y, movedAsset.rotation.z]
},
timeStap: new Date().toISOString()
});
}
const newFloorItem: Types.FloorItemType = {
modelUuid: movedAsset.userData.modelUuid,
@@ -339,7 +405,7 @@ function MoveControls3D({
}
updateAsset(movedAsset.userData.modelUuid, {
position: asset.position,
position: [position.x, position.y, position.z],
rotation: [movedAsset.rotation.x, movedAsset.rotation.y, movedAsset.rotation.z],
});
@@ -368,9 +434,31 @@ function MoveControls3D({
}
});
if (assetsToUpdate.length > 0) {
if (assetsToUpdate.length === 1) {
undoActions.push({
module: "builder",
actionType: "Asset-Update",
asset: assetsToUpdate[0]
});
} else {
undoActions.push({
module: "builder",
actionType: "Assets-Update",
assets: assetsToUpdate
});
}
push3D({
type: 'Scene',
actions: undoActions
});
}
echo.success("Object moved!");
setIsMoving(false);
clearSelection();
setAxisConstraint(null);
};
const clearSelection = () => {

View File

@@ -1,7 +1,7 @@
import * as THREE from "three";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import { useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView } from "../../../../../store/builder/store";
import * as Types from "../../../../../types/world/worldTypes";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
import { useParams } from "react-router-dom";
@@ -9,6 +9,8 @@ import { useProductContext } from "../../../../simulation/products/productContex
import { getUserData } from "../../../../../functions/getUserData";
import { useSceneContext } from "../../../sceneContext";
import { useVersionContext } from "../../../../builder/version/versionContext";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import { handleAssetRotationSnap } from "./functions/handleAssetRotationSnap";
// import { setAssetsApi } from '../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi';
@@ -23,7 +25,6 @@ function RotateControls3D({
setDuplicatedObjects
}: any) {
const { camera, gl, scene, pointer, raycaster } = useThree();
const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []);
const { toggleView } = useToggleView();
const { selectedAssets, setSelectedAssets } = useSelectedAssets();
@@ -32,20 +33,23 @@ function RotateControls3D({
const { socket } = useSocketStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
const { assetStore, eventStore, productStore } = useSceneContext();
const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { updateAsset } = assetStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">("");
const [initialRotations, setInitialRotations] = useState<Record<string, THREE.Euler>>({});
const [initialPositions, setInitialPositions] = useState<Record<string, THREE.Vector3>>({});
const [isRotating, setIsRotating] = useState(false);
const prevPointerPosition = useRef<THREE.Vector2 | null>(null);
const rotationCenter = useRef<THREE.Vector3 | null>(null);
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({
left: false,
right: false,
});
const mouseButtonsDown = useRef<{ left: boolean; right: boolean }>({ left: false, right: false, });
const { contextAction, setContextAction } = useContextActionStore()
const snapBaseRef = useRef<number | null>(null);
const prevRotationRef = useRef<number | null>(null);
const wasShiftHeldRef = useRef(false);
const updateBackend = useCallback((
productName: string,
@@ -62,6 +66,13 @@ function RotateControls3D({
});
}, [selectedVersion]);
useEffect(() => {
if (contextAction === "rotateAsset") {
setContextAction(null);
rotateAssets()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
@@ -97,6 +108,8 @@ function RotateControls3D({
};
const onKeyDown = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || movedObjects.length > 0) return;
if (event.key.toLowerCase() === "r") {
@@ -104,6 +117,15 @@ function RotateControls3D({
rotateAssets();
}
}
if (keyCombination !== keyEvent) {
if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") {
setKeyEvent(keyCombination);
} else {
setKeyEvent("");
}
}
if (event.key.toLowerCase() === "escape") {
event.preventDefault();
resetToInitialRotations();
@@ -112,11 +134,24 @@ function RotateControls3D({
}
};
const onKeyUp = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (keyCombination === "") {
setKeyEvent("");
} else if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") {
setKeyEvent(keyCombination);
}
const currentPointer = new THREE.Vector2(pointer.x, pointer.y);
prevPointerPosition.current = currentPointer.clone();
};
if (!toggleView) {
canvasElement.addEventListener("pointerdown", onPointerDown);
canvasElement.addEventListener("pointermove", onPointerMove);
canvasElement.addEventListener("pointerup", onPointerUp);
canvasElement.addEventListener("keydown", onKeyDown);
canvasElement?.addEventListener("keyup", onKeyUp);
}
return () => {
@@ -124,68 +159,72 @@ function RotateControls3D({
canvasElement.removeEventListener("pointermove", onPointerMove);
canvasElement.removeEventListener("pointerup", onPointerUp);
canvasElement.removeEventListener("keydown", onKeyDown);
canvasElement?.removeEventListener("keyup", onKeyUp);
};
}, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects]);
}, [camera, scene, toggleView, selectedAssets, rotatedObjects, pastedObjects, duplicatedObjects, movedObjects, keyEvent]);
const resetToInitialRotations = useCallback(() => {
rotatedObjects.forEach((obj: THREE.Object3D) => {
const uuid = obj.uuid;
if (obj.userData.modelUuid) {
const initialRotation = initialRotations[uuid];
const initialPosition = initialPositions[uuid];
setTimeout(() => {
rotatedObjects.forEach((obj: THREE.Object3D) => {
const uuid = obj.uuid;
if (obj.userData.modelUuid) {
const initialRotation = initialRotations[uuid];
const initialPosition = initialPositions[uuid];
if (initialRotation && initialPosition) {
const rotationArray: [number, number, number] = [initialRotation.x, initialRotation.y, initialRotation.z,];
const positionArray: [number, number, number] = [initialPosition.x, initialPosition.y, initialPosition.z,];
if (initialRotation && initialPosition) {
const rotationArray: [number, number, number] = [initialRotation.x, initialRotation.y, initialRotation.z,];
const positionArray: [number, number, number] = [initialPosition.x, initialPosition.y, initialPosition.z,];
updateAsset(obj.userData.modelUuid, {
rotation: rotationArray,
position: positionArray,
});
updateAsset(obj.userData.modelUuid, {
rotation: rotationArray,
position: positionArray,
});
obj.rotation.copy(initialRotation);
obj.position.copy(initialPosition);
obj.rotation.copy(initialRotation);
obj.position.copy(initialPosition);
}
}
}
});
});
}, 100)
}, [rotatedObjects, initialRotations, initialPositions, updateAsset]);
useFrame(() => {
if (!isRotating || rotatedObjects.length === 0) return;
const intersectionPoint = new THREE.Vector3();
raycaster.setFromCamera(pointer, camera);
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
const currentPointer = new THREE.Vector2(pointer.x, pointer.y);
if (mouseButtonsDown.current.left || mouseButtonsDown.current.right) {
if (point) {
prevPointerPosition.current = new THREE.Vector2(point.x, point.z);
}
prevPointerPosition.current = currentPointer.clone();
return;
}
if (point && prevPointerPosition.current && rotationCenter.current) {
if (prevPointerPosition.current && rotationCenter.current) {
const center = rotationCenter.current;
const currentAngle = Math.atan2(point.z - center.z, point.x - center.x);
const prevAngle = Math.atan2(
prevPointerPosition.current.y - center.z,
prevPointerPosition.current.x - center.x
);
const angleDelta = prevAngle - currentAngle;
const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta);
const deltaX = currentPointer.x - prevPointerPosition.current.x;
rotatedObjects.forEach((obj: THREE.Object3D) => {
if (obj.userData.modelUuid) {
const angleDelta = handleAssetRotationSnap({
object: obj,
pointerDeltaX: deltaX,
keyEvent,
snapBaseRef,
prevRotationRef,
wasShiftHeldRef
});
const relativePosition = new THREE.Vector3().subVectors(obj.position, center);
const rotationMatrix = new THREE.Matrix4().makeRotationY(angleDelta);
relativePosition.applyMatrix4(rotationMatrix);
obj.position.copy(center).add(relativePosition);
obj.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), angleDelta);
const rotationQuat = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), angleDelta);
obj.quaternion.multiply(rotationQuat);
obj.rotation.setFromQuaternion(obj.quaternion);
}
});
prevPointerPosition.current = new THREE.Vector2(point.x, point.z);
prevPointerPosition.current = currentPointer.clone();
}
});
@@ -207,27 +246,54 @@ function RotateControls3D({
setInitialRotations(rotations);
setInitialPositions(positions);
const intersectionPoint = new THREE.Vector3();
raycaster.setFromCamera(pointer, camera);
const point = raycaster.ray.intersectPlane(plane, intersectionPoint);
if (point) {
prevPointerPosition.current = new THREE.Vector2(point.x, point.z);
}
prevPointerPosition.current = new THREE.Vector2(pointer.x, pointer.y);
setRotatedObjects(selectedAssets);
setIsRotating(true);
}, [selectedAssets, camera, pointer, raycaster, plane]);
}, [selectedAssets, camera, pointer, raycaster]);
const placeRotatedAssets = useCallback(() => {
if (rotatedObjects.length === 0) return;
const undoActions: UndoRedo3DAction[] = [];
const assetsToUpdate: AssetData[] = [];
rotatedObjects.forEach((obj: THREE.Object3D) => {
if (obj && obj.userData.modelUuid) {
const asset = assetStore.getState().getAssetById(obj.userData.modelUuid);
if (!asset) return;
const rotationArray: [number, number, number] = [obj.rotation.x, obj.rotation.y, obj.rotation.z];
const positionArray: [number, number, number] = [obj.position.x, obj.position.y, obj.position.z];
if (initialRotations[obj.uuid] && initialPositions[obj.uuid]) {
assetsToUpdate.push({
type: "Asset",
assetData: {
...asset,
position: [
initialPositions[obj.uuid].x,
initialPositions[obj.uuid].y,
initialPositions[obj.uuid].z
],
rotation: [
initialRotations[obj.uuid].x,
initialRotations[obj.uuid].y,
initialRotations[obj.uuid].z
]
},
newData: {
...asset,
position: positionArray,
rotation: rotationArray
},
timeStap: new Date().toISOString()
});
}
const newFloorItem: Types.FloorItemType = {
modelUuid: obj.userData.modelUuid,
modelName: obj.userData.modelName,
@@ -305,6 +371,27 @@ function RotateControls3D({
}
});
if (assetsToUpdate.length > 0) {
if (assetsToUpdate.length === 1) {
undoActions.push({
module: "builder",
actionType: "Asset-Update",
asset: assetsToUpdate[0]
});
} else {
undoActions.push({
module: "builder",
actionType: "Assets-Update",
assets: assetsToUpdate
});
}
push3D({
type: 'Scene',
actions: undoActions
});
}
setIsRotating(false);
clearSelection();
}, [rotatedObjects, eventStore, productStore, selectedProduct, updateBackend, projectId, updateAsset, organization, socket, selectedVersion, userId]);

View File

@@ -10,7 +10,7 @@ import { getUserData } from "../../../../../functions/getUserData";
import { useSceneContext } from "../../../sceneContext";
import { useVersionContext } from "../../../../builder/version/versionContext";
import { useProductContext } from "../../../../simulation/products/productContext";
import { useSelectedAssets, useSocketStore, useToggleView, useToolMode, } from "../../../../../store/builder/store";
import { useContextActionStore, useSelectedAssets, useSocketStore, useToggleView, useToolMode, } from "../../../../../store/builder/store";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
import DuplicationControls3D from "./duplicationControls3D";
import CopyPasteControls3D from "./copyPasteControls3D";
@@ -31,8 +31,10 @@ const SelectionControls3D: React.FC = () => {
const boundingBoxRef = useRef<THREE.Mesh>();
const { activeModule } = useModuleStore();
const { socket } = useSocketStore();
const { assetStore, eventStore, productStore } = useSceneContext();
const { removeAsset } = assetStore();
const { contextAction, setContextAction } = useContextActionStore()
const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { removeAsset, getAssetById } = assetStore();
const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]);
const { toolMode } = useToolMode();
const { selectedVersionStore } = useVersionContext();
@@ -65,6 +67,13 @@ const SelectionControls3D: React.FC = () => {
});
};
useEffect(() => {
if (contextAction === "deleteAsset") {
setContextAction(null);
deleteSelection()
}
}, [contextAction])
useEffect(() => {
if (!camera || !scene || toggleView) return;
@@ -107,7 +116,7 @@ const SelectionControls3D: React.FC = () => {
if (event.button === 2 && !event.ctrlKey && !event.shiftKey) {
isRightClick.current = false;
if (!rightClickMoved.current) {
clearSelection();
// clearSelection();
}
return;
}
@@ -192,7 +201,7 @@ const SelectionControls3D: React.FC = () => {
const onContextMenu = (event: MouseEvent) => {
event.preventDefault();
if (!rightClickMoved.current) {
clearSelection();
// clearSelection();
}
rightClickMoved.current = false;
};
@@ -266,12 +275,18 @@ const SelectionControls3D: React.FC = () => {
const deleteSelection = () => {
if (selectedAssets.length > 0 && duplicatedObjects.length === 0) {
const undoActions: UndoRedo3DAction[] = [];
const assetsToDelete: AssetData[] = [];
const selectedUUIDs = selectedAssets.map((mesh: THREE.Object3D) => mesh.uuid);
selectedAssets.forEach((selectedMesh: THREE.Object3D) => {
const asset = getAssetById(selectedMesh.userData.modelUuid);
if (!asset) return;
//REST
// const response = await deleteFloorItem(organization, selectedMesh.userData.modelUuid, selectedMesh.userData.modelName);
// const response = deleteFloorItem(organization, selectedMesh.userData.modelUuid, selectedMesh.userData.modelName);
//SOCKET
@@ -321,6 +336,31 @@ const SelectionControls3D: React.FC = () => {
}
});
assetsToDelete.push({
type: "Asset",
assetData: asset,
timeStap: new Date().toISOString()
});
});
if (assetsToDelete.length === 1) {
undoActions.push({
module: "builder",
actionType: "Asset-Delete",
asset: assetsToDelete[0]
});
} else {
undoActions.push({
module: "builder",
actionType: "Assets-Delete",
assets: assetsToDelete
});
}
push3D({
type: 'Scene',
actions: undoActions
});
selectedUUIDs.forEach((uuid: string) => {

View File

@@ -3,7 +3,7 @@ import * as THREE from "three";
import { useSelectedFloorItem, useObjectPosition, useObjectRotation, useActiveTool, useSocketStore } from "../../../../store/builder/store";
import { useThree } from "@react-three/fiber";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifierKeys";
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
// import { setAssetsApi } from "../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
@@ -15,6 +15,7 @@ import { useVersionContext } from "../../../builder/version/versionContext";
export default function TransformControl() {
const state = useThree();
const ref = useRef(null);
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
const { selectedFloorItem, setSelectedFloorItem } = useSelectedFloorItem();
const { setObjectPosition } = useObjectPosition();
@@ -23,7 +24,8 @@ export default function TransformControl() {
const { socket } = useSocketStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { assetStore, eventStore, productStore } = useSceneContext();
const { assetStore, eventStore, productStore, undoRedo3DStore } = useSceneContext();
const { push3D } = undoRedo3DStore();
const { updateAsset, getAssetById } = assetStore();
const { userId, organization } = getUserData();
const { selectedVersionStore } = useVersionContext();
@@ -136,13 +138,37 @@ export default function TransformControl() {
projectId
};
// console.log('data: ', data);
socket.emit("v1:model-asset:add", data);
push3D({
type: 'Scene',
actions: [
{
module: "builder",
actionType: "Asset-Update",
asset: {
type: "Asset",
assetData: asset,
newData: {
...asset,
position: [selectedFloorItem.position.x, 0, selectedFloorItem.position.z],
rotation: [selectedFloorItem.rotation.x, selectedFloorItem.rotation.y, selectedFloorItem.rotation.z],
},
timeStap: new Date().toISOString()
}
}
]
});
}
}
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const isTextInput = (element: Element | null): boolean =>
element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element?.getAttribute("contenteditable") === "true";
if (isTextInput(document.activeElement)) return;
const keyCombination = detectModifierKeys(e);
if (!selectedFloorItem) return;
if (keyCombination === "G") {
@@ -186,8 +212,9 @@ export default function TransformControl() {
<>
{(selectedFloorItem && transformMode) &&
<TransformControls
ref={ref}
showX={transformMode === "translate"}
showY={transformMode === "rotate"}
showY={transformMode === "rotate" || transformMode === "translate"}
showZ={transformMode === "translate"}
object={selectedFloorItem}
mode={transformMode}

View File

@@ -16,7 +16,7 @@ import { useSocketStore } from "../../../../../store/builder/store";
// import { upsertAisleApi } from "../../../../../services/factoryBuilder/aisle/upsertAisleApi";
// import { deleteAisleApi } from "../../../../../services/factoryBuilder/aisle/deleteAisleApi";
function useRedoHandler() {
function use2DRedoHandler() {
const { undoRedo2DStore, wallStore, floorStore, zoneStore, aisleStore } = useSceneContext();
const { redo2D, peekRedo2D } = undoRedo2DStore();
const { addWall, removeWall, updateWall } = wallStore();
@@ -352,4 +352,4 @@ function useRedoHandler() {
return { handleRedo };
}
export default useRedoHandler;
export default use2DRedoHandler;

View File

@@ -16,7 +16,7 @@ import { useSocketStore } from "../../../../../store/builder/store";
// import { upsertAisleApi } from "../../../../../services/factoryBuilder/aisle/upsertAisleApi";
// import { deleteAisleApi } from "../../../../../services/factoryBuilder/aisle/deleteAisleApi";
function useUndoHandler() {
function use2DUndoHandler() {
const { undoRedo2DStore, wallStore, floorStore, zoneStore, aisleStore } = useSceneContext();
const { undo2D, peekUndo2D } = undoRedo2DStore();
const { addWall, removeWall, updateWall } = wallStore();
@@ -67,38 +67,37 @@ function useUndoHandler() {
}
undo2D();
};
const handleCreate = (point: UndoRedo2DDataTypeSchema) => {
switch (point.type) {
case 'Wall': createWallFromBackend(point.lineData); break;
case 'Floor': createFloorFromBackend(point.lineData); break;
case 'Zone': createZoneFromBackend(point.lineData); break;
case 'Aisle': createAisleFromBackend(point.lineData); break;
case 'Wall': createWallToBackend(point.lineData); break;
case 'Floor': createFloorToBackend(point.lineData); break;
case 'Zone': createZoneToBackend(point.lineData); break;
case 'Aisle': createAisleToBackend(point.lineData); break;
}
};
const handleRemove = (point: UndoRedo2DDataTypeSchema) => {
switch (point.type) {
case 'Wall': removeWallFromBackend(point.lineData.wallUuid); break;
case 'Floor': removeFloorFromBackend(point.lineData.floorUuid); break;
case 'Zone': removeZoneFromBackend(point.lineData.zoneUuid); break;
case 'Aisle': removeAisleFromBackend(point.lineData.aisleUuid); break;
case 'Wall': removeWallToBackend(point.lineData.wallUuid); break;
case 'Floor': removeFloorToBackend(point.lineData.floorUuid); break;
case 'Zone': removeZoneToBackend(point.lineData.zoneUuid); break;
case 'Aisle': removeAisleToBackend(point.lineData.aisleUuid); break;
}
};
const handleUpdate = (point: UndoRedo2DDataTypeSchema) => {
switch (point.type) {
case 'Wall': updateWallFromBackend(point.lineData.wallUuid, point.lineData); break;
case 'Floor': updateFloorFromBackend(point.lineData.floorUuid, point.lineData); break;
case 'Zone': updateZoneFromBackend(point.lineData.zoneUuid, point.lineData); break;
case 'Aisle': updateAisleFromBackend(point.lineData.aisleUuid, point.lineData); break;
case 'Wall': updateWallToBackend(point.lineData.wallUuid, point.lineData); break;
case 'Floor': updateFloorToBackend(point.lineData.floorUuid, point.lineData); break;
case 'Zone': updateZoneToBackend(point.lineData.zoneUuid, point.lineData); break;
case 'Aisle': updateAisleToBackend(point.lineData.aisleUuid, point.lineData); break;
}
};
const createWallFromBackend = (wallData: Wall) => {
const createWallToBackend = (wallData: Wall) => {
addWall(wallData);
if (projectId) {
// API
@@ -119,7 +118,7 @@ function useUndoHandler() {
}
};
const removeWallFromBackend = (wallUuid: string) => {
const removeWallToBackend = (wallUuid: string) => {
removeWall(wallUuid);
if (projectId) {
// API
@@ -140,7 +139,7 @@ function useUndoHandler() {
}
};
const updateWallFromBackend = (wallUuid: string, updatedData: Wall) => {
const updateWallToBackend = (wallUuid: string, updatedData: Wall) => {
updateWall(wallUuid, updatedData);
if (projectId) {
// API
@@ -161,7 +160,7 @@ function useUndoHandler() {
}
};
const createFloorFromBackend = (floorData: Floor) => {
const createFloorToBackend = (floorData: Floor) => {
addFloor(floorData);
if (projectId) {
// API
@@ -182,7 +181,7 @@ function useUndoHandler() {
}
};
const removeFloorFromBackend = (floorUuid: string) => {
const removeFloorToBackend = (floorUuid: string) => {
removeFloor(floorUuid);
if (projectId) {
// API
@@ -203,7 +202,7 @@ function useUndoHandler() {
}
};
const updateFloorFromBackend = (floorUuid: string, updatedData: Floor) => {
const updateFloorToBackend = (floorUuid: string, updatedData: Floor) => {
updateFloor(floorUuid, updatedData);
if (projectId) {
// API
@@ -224,7 +223,7 @@ function useUndoHandler() {
}
};
const createZoneFromBackend = (zoneData: Zone) => {
const createZoneToBackend = (zoneData: Zone) => {
addZone(zoneData);
if (projectId) {
// API
@@ -245,7 +244,7 @@ function useUndoHandler() {
}
};
const removeZoneFromBackend = (zoneUuid: string) => {
const removeZoneToBackend = (zoneUuid: string) => {
removeZone(zoneUuid);
if (projectId) {
// API
@@ -266,7 +265,7 @@ function useUndoHandler() {
}
};
const updateZoneFromBackend = (zoneUuid: string, updatedData: Zone) => {
const updateZoneToBackend = (zoneUuid: string, updatedData: Zone) => {
updateZone(zoneUuid, updatedData);
if (projectId) {
// API
@@ -287,7 +286,7 @@ function useUndoHandler() {
}
};
const createAisleFromBackend = (aisleData: Aisle) => {
const createAisleToBackend = (aisleData: Aisle) => {
addAisle(aisleData);
if (projectId) {
// API
@@ -308,7 +307,7 @@ function useUndoHandler() {
}
};
const removeAisleFromBackend = (aisleUuid: string) => {
const removeAisleToBackend = (aisleUuid: string) => {
removeAisle(aisleUuid);
if (projectId) {
// API
@@ -329,7 +328,7 @@ function useUndoHandler() {
}
};
const updateAisleFromBackend = (aisleUuid: string, updatedData: Aisle) => {
const updateAisleToBackend = (aisleUuid: string, updatedData: Aisle) => {
updateAisle(aisleUuid, updatedData);
if (projectId) {
// API
@@ -353,4 +352,4 @@ function useUndoHandler() {
return { handleUndo };
}
export default useUndoHandler;
export default use2DUndoHandler;

View File

@@ -0,0 +1,318 @@
import { useParams } from "react-router-dom";
import { getUserData } from "../../../../../functions/getUserData";
import { useVersionContext } from "../../../../builder/version/versionContext";
import { useSceneContext } from "../../../sceneContext";
import { useProductContext } from "../../../../simulation/products/productContext";
import { useSocketStore } from "../../../../../store/builder/store";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
// import { setAssetsApi } from "../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
// import { deleteFloorItem } from "../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorItemApi";
function use3DRedoHandler() {
const { undoRedo3DStore, assetStore, productStore, eventStore } = useSceneContext();
const { deleteEvent } = productStore();
const { addEvent, removeEvent } = eventStore();
const { updateAsset, removeAsset, addAsset } = assetStore();
const { redo3D, peekRedo3D } = undoRedo3DStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
const { socket } = useSocketStore();
const updateBackend = (
productName: string,
productUuid: string,
projectId: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName: productName,
productUuid: productUuid,
projectId: projectId,
eventDatas: eventData,
versionId: selectedVersion?.versionId || '',
});
};
const handleRedo = () => {
const redoData = peekRedo3D();
if (!redoData) return;
if (redoData.type === 'Scene') {
const { actions } = redoData;
actions.forEach(action => {
const { actionType } = action;
if ('asset' in action) {
const asset = action.asset;
if (actionType === 'Asset-Add') {
handleAdd(asset);
} else if (actionType === 'Asset-Delete') {
handleDelete(asset);
} else if (actionType === 'Asset-Update') {
handleUpdate(asset);
} else if (actionType === 'Asset-Copied') {
handleCopy(asset);
} else if (actionType === 'Asset-Duplicated') {
handleDuplicate(asset);
}
} else if ('assets' in action) {
const assets = action.assets;
if (actionType === 'Assets-Add') {
assets.forEach(handleAdd);
} else if (actionType === 'Assets-Delete') {
assets.forEach(handleDelete);
} else if (actionType === 'Assets-Update') {
assets.forEach(handleUpdate);
} else if (actionType === 'Assets-Copied') {
assets.forEach(handleCopy);
} else if (actionType === 'Assets-Duplicated') {
assets.forEach(handleDuplicate);
}
}
});
} else if (redoData.type === 'UI') {
// Handle UI actions if needed
}
redo3D();
};
const handleAdd = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': addAssetToBackend(asset.assetData); break;
case 'WallAsset': addWallAssetToBackend(asset.assetData); break;
}
};
const handleDelete = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': deleteAssetToBackend(asset.assetData); break;
case 'WallAsset': deleteWallAssetToBackend(asset.assetData); break;
}
}
const handleUpdate = (asset: AssetData) => {
if (!asset.newData) return;
switch (asset.type) {
case 'Asset': updateAssetToBackend(asset.newData.modelUuid, asset.newData); break;
case 'WallAsset': updateWallAssetToBackend(asset.newData.modelUuid, asset.newData); break;
}
}
const handleCopy = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': copyAssetToBackend(asset.assetData); break;
case 'WallAsset': copyWallAssetToBackend(asset.assetData); break;
}
}
const handleDuplicate = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': duplicateAssetToBackend(asset.assetData); break;
case 'WallAsset': duplicateWallAssetToBackend(asset.assetData); break;
}
}
const addAssetToBackend = (assetData: Asset) => {
addAsset(assetData);
if (projectId) {
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
assetId: assetData.assetId,
position: assetData.position,
rotation: { x: assetData.rotation[0], y: assetData.rotation[1], z: assetData.rotation[2] },
isLocked: false,
isVisible: true,
eventData: {},
socketId: socket.id,
versionId: selectedVersion?.versionId || '',
projectId,
userId
};
if (assetData.eventData) {
data.eventData = assetData.eventData;
addEvent(assetData.eventData as EventsSchema);
}
// API
// setAssetsApi(data);
//SOCKET
socket.emit("v1:model-asset:add", data);
}
}
const deleteAssetToBackend = (assetData: Asset) => {
//REST
// const response = deleteFloorItem(organization, assetData.modelUuid, assetData.modelName);
//SOCKET
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
socketId: socket.id,
userId,
versionId: selectedVersion?.versionId || '',
projectId
}
const response = socket.emit('v1:model-asset:delete', data)
removeEvent(assetData.modelUuid);
const updatedEvents = deleteEvent(assetData.modelUuid);
updatedEvents.forEach((event) => {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
})
if (response) {
removeAsset(assetData.modelUuid);
}
}
const updateAssetToBackend = (modelUuid: string, updatedData: Asset) => {
updateAsset(modelUuid, updatedData);
if (projectId) {
const data = {
organization,
modelUuid: updatedData.modelUuid,
modelName: updatedData.modelName,
assetId: updatedData.assetId,
position: updatedData.position,
rotation: { x: updatedData.rotation[0], y: updatedData.rotation[1], z: updatedData.rotation[2] },
isLocked: false,
isVisible: true,
socketId: socket.id,
versionId: selectedVersion?.versionId || '',
projectId,
userId
};
// API
// setAssetsApi(data);
//SOCKET
socket.emit("v1:model-asset:add", data);
}
}
const copyAssetToBackend = (assetData: Asset) => {
addAsset(assetData);
if (projectId) {
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
assetId: assetData.assetId,
position: assetData.position,
rotation: { x: assetData.rotation[0], y: assetData.rotation[1], z: assetData.rotation[2] },
isLocked: false,
isVisible: true,
eventData: {},
socketId: socket.id,
versionId: selectedVersion?.versionId || '',
projectId,
userId
};
if (assetData.eventData) {
data.eventData = assetData.eventData;
addEvent(assetData.eventData as EventsSchema);
}
// API
// setAssetsApi(data);
//SOCKET
socket.emit("v1:model-asset:add", data);
}
}
const duplicateAssetToBackend = (assetData: Asset) => {
addAsset(assetData);
if (projectId) {
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
assetId: assetData.assetId,
position: assetData.position,
rotation: { x: assetData.rotation[0], y: assetData.rotation[1], z: assetData.rotation[2] },
isLocked: false,
isVisible: true,
eventData: {},
socketId: socket.id,
versionId: selectedVersion?.versionId || '',
projectId,
userId
};
if (assetData.eventData) {
data.eventData = assetData.eventData;
addEvent(assetData.eventData as EventsSchema);
}
// API
// setAssetsApi(data);
//SOCKET
socket.emit("v1:model-asset:add", data);
}
}
const addWallAssetToBackend = (assetData: WallAsset) => {
}
const deleteWallAssetToBackend = (assetData: WallAsset) => {
}
const updateWallAssetToBackend = (modelUuid: string, updatedData: WallAsset) => {
}
const copyWallAssetToBackend = (assetData: WallAsset) => {
}
const duplicateWallAssetToBackend = (assetData: WallAsset) => {
}
return { handleRedo };
}
export default use3DRedoHandler;

View File

@@ -0,0 +1,323 @@
import { useParams } from "react-router-dom";
import { getUserData } from "../../../../../functions/getUserData";
import { useVersionContext } from "../../../../builder/version/versionContext";
import { useSceneContext } from "../../../sceneContext";
import { useProductContext } from "../../../../simulation/products/productContext";
import { useSocketStore } from "../../../../../store/builder/store";
import { upsertProductOrEventApi } from "../../../../../services/simulation/products/UpsertProductOrEventApi";
// import { setAssetsApi } from "../../../../../services/factoryBuilder/asset/floorAsset/setAssetsApi";
// import { deleteFloorItem } from "../../../../../services/factoryBuilder/asset/floorAsset/deleteFloorItemApi";
function use3DUndoHandler() {
const { undoRedo3DStore, assetStore, productStore, eventStore } = useSceneContext();
const { deleteEvent } = productStore();
const { addEvent, removeEvent } = eventStore();
const { updateAsset, removeAsset, addAsset } = assetStore();
const { undo3D, peekUndo3D } = undoRedo3DStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { userId, organization } = getUserData();
const { projectId } = useParams();
const { socket } = useSocketStore();
const updateBackend = (
productName: string,
productUuid: string,
projectId: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName: productName,
productUuid: productUuid,
projectId: projectId,
eventDatas: eventData,
versionId: selectedVersion?.versionId || '',
});
};
const handleUndo = () => {
const unDoData = peekUndo3D();
if (!unDoData) return;
if (unDoData.type === 'Scene') {
const { actions } = unDoData;
actions.forEach(action => {
const { actionType } = action;
if ('asset' in action) {
const asset = action.asset;
if (actionType === 'Asset-Add') {
handleDelete(asset);
} else if (actionType === 'Asset-Delete') {
handleAdd(asset);
} else if (actionType === 'Asset-Update') {
handleUpdate(asset);
} else if (actionType === 'Asset-Copied') {
handleCopy(asset);
} else if (actionType === 'Asset-Duplicated') {
handleDuplicate(asset);
}
} else if ('assets' in action) {
const assets = action.assets;
if (actionType === 'Assets-Add') {
assets.forEach(handleDelete);
} else if (actionType === 'Assets-Delete') {
assets.forEach(handleAdd);
} else if (actionType === 'Assets-Update') {
assets.forEach(handleUpdate);
} else if (actionType === 'Assets-Copied') {
assets.forEach(handleCopy);
} else if (actionType === 'Assets-Duplicated') {
assets.forEach(handleDuplicate);
}
}
});
} else if (unDoData.type === 'UI') {
// Handle UI actions if needed
}
undo3D();
};
const handleAdd = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': addAssetToBackend(asset.assetData); break;
case 'WallAsset': addWallAssetToBackend(asset.assetData); break;
}
};
const handleDelete = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': deleteAssetToBackend(asset.assetData); break;
case 'WallAsset': deleteWallAssetToBackend(asset.assetData); break;
}
}
const handleUpdate = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': updateAssetToBackend(asset.assetData.modelUuid, asset.assetData); break;
case 'WallAsset': updateWallAssetToBackend(asset.assetData.modelUuid, asset.assetData); break;
}
}
const handleCopy = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': copyAssetToBackend(asset.assetData); break;
case 'WallAsset': copyWallAssetToBackend(asset.assetData); break;
}
}
const handleDuplicate = (asset: AssetData) => {
switch (asset.type) {
case 'Asset': duplicateAssetToBackend(asset.assetData); break;
case 'WallAsset': duplicateWallAssetToBackend(asset.assetData); break;
}
}
const addAssetToBackend = (assetData: Asset) => {
addAsset(assetData);
if (projectId) {
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
assetId: assetData.assetId,
position: assetData.position,
rotation: { x: assetData.rotation[0], y: assetData.rotation[1], z: assetData.rotation[2] },
isLocked: false,
isVisible: true,
eventData: {},
socketId: socket.id,
versionId: selectedVersion?.versionId || '',
projectId,
userId
};
if (assetData.eventData) {
data.eventData = assetData.eventData;
addEvent(assetData.eventData as EventsSchema);
}
// API
// setAssetsApi(data);
//SOCKET
socket.emit("v1:model-asset:add", data);
}
}
const deleteAssetToBackend = (assetData: Asset) => {
//REST
// const response = deleteFloorItem(organization, assetData.modelUuid, assetData.modelName);
//SOCKET
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
socketId: socket.id,
userId,
versionId: selectedVersion?.versionId || '',
projectId
}
const response = socket.emit('v1:model-asset:delete', data)
removeEvent(assetData.modelUuid);
const updatedEvents = deleteEvent(assetData.modelUuid);
updatedEvents.forEach((event) => {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
})
if (response) {
removeAsset(assetData.modelUuid);
}
}
const updateAssetToBackend = (modelUuid: string, updatedData: Asset) => {
updateAsset(modelUuid, updatedData);
if (projectId) {
const data = {
organization,
modelUuid: updatedData.modelUuid,
modelName: updatedData.modelName,
assetId: updatedData.assetId,
position: updatedData.position,
rotation: { x: updatedData.rotation[0], y: updatedData.rotation[1], z: updatedData.rotation[2] },
isLocked: false,
isVisible: true,
socketId: socket.id,
versionId: selectedVersion?.versionId || '',
projectId,
userId
};
// API
// setAssetsApi(data);
//SOCKET
socket.emit("v1:model-asset:add", data);
}
}
const copyAssetToBackend = (assetData: Asset) => {
//REST
// const response = deleteFloorItem(organization, assetData.modelUuid, assetData.modelName);
//SOCKET
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
socketId: socket.id,
userId,
versionId: selectedVersion?.versionId || '',
projectId
}
const response = socket.emit('v1:model-asset:delete', data)
removeEvent(assetData.modelUuid);
const updatedEvents = deleteEvent(assetData.modelUuid);
updatedEvents.forEach((event) => {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
})
if (response) {
removeAsset(assetData.modelUuid);
}
}
const duplicateAssetToBackend = (assetData: Asset) => {
//REST
// const response = deleteFloorItem(organization, assetData.modelUuid, assetData.modelName);
//SOCKET
const data = {
organization,
modelUuid: assetData.modelUuid,
modelName: assetData.modelName,
socketId: socket.id,
userId,
versionId: selectedVersion?.versionId || '',
projectId
}
const response = socket.emit('v1:model-asset:delete', data)
removeEvent(assetData.modelUuid);
const updatedEvents = deleteEvent(assetData.modelUuid);
updatedEvents.forEach((event) => {
updateBackend(
selectedProduct.productName,
selectedProduct.productUuid,
projectId || '',
event
);
})
if (response) {
removeAsset(assetData.modelUuid);
}
}
const addWallAssetToBackend = (assetData: WallAsset) => {
}
const deleteWallAssetToBackend = (assetData: WallAsset) => {
}
const updateWallAssetToBackend = (modelUuid: string, updatedData: WallAsset) => {
}
const copyWallAssetToBackend = (assetData: WallAsset) => {
}
const duplicateWallAssetToBackend = (assetData: WallAsset) => {
}
return { handleUndo };
}
export default use3DUndoHandler;

View File

@@ -4,15 +4,15 @@ import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModi
import { useSocketStore, useToggleView } from '../../../../../store/builder/store';
import { useVersionContext } from '../../../../builder/version/versionContext';
import useUndoHandler from '../handlers/useUndoHandler';
import useRedoHandler from '../handlers/useRedoHandler';
import use2DUndoHandler from '../handlers/use2DUndoHandler';
import use2DRedoHandler from '../handlers/use2DRedoHandler';
function UndoRedo2DControls() {
const { undoRedo2DStore } = useSceneContext();
const { undoStack, redoStack } = undoRedo2DStore();
const { toggleView } = useToggleView();
const { handleUndo } = useUndoHandler();
const { handleRedo } = useRedoHandler();
const { handleUndo } = use2DUndoHandler();
const { handleRedo } = use2DRedoHandler();
const { socket } = useSocketStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();

View File

@@ -0,0 +1,51 @@
import { useEffect } from 'react'
import { useSceneContext } from '../../../sceneContext'
import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
import { useSocketStore, useToggleView } from '../../../../../store/builder/store';
import { useVersionContext } from '../../../../builder/version/versionContext';
import useModuleStore from '../../../../../store/useModuleStore';
import use3DUndoHandler from '../handlers/use3DUndoHandler';
import use3DRedoHandler from '../handlers/use3DRedoHandler';
function UndoRedo3DControls() {
const { undoRedo3DStore } = useSceneContext();
const { undoStack, redoStack } = undoRedo3DStore();
const { toggleView } = useToggleView();
const { activeModule } = useModuleStore();
const { handleUndo } = use3DUndoHandler();
const { handleRedo } = use3DRedoHandler();
const { socket } = useSocketStore();
const { selectedVersionStore } = useVersionContext();
const { selectedVersion } = selectedVersionStore();
useEffect(() => {
console.log(undoStack, redoStack);
}, [undoStack, redoStack]);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (keyCombination === 'Ctrl+Z') {
handleUndo();
}
if (keyCombination === 'Ctrl+Y') {
handleRedo();
}
};
if (!toggleView) {
window.addEventListener('keydown', handleKeyDown);
}
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [toggleView, undoStack, redoStack, socket, selectedVersion, activeModule]);
return null;
}
export default UndoRedo3DControls;

View File

@@ -0,0 +1,22 @@
import { useEffect, useState } from "react";
import { Stats } from "@react-three/drei";
import { detectModifierKeys } from "../../../utils/shortcutkeys/detectModifierKeys";
export default function StatsHelper() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const keyCombination = detectModifierKeys(event);
if (keyCombination === "F1") {
event.preventDefault();
setVisible(prev => !prev);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, []);
return visible ? <Stats className="stats" /> : null;
}

View File

@@ -2,8 +2,7 @@ import { useEffect, useMemo } from "react";
import { Canvas } from "@react-three/fiber";
import { Physics } from "@react-three/rapier";
import { Color, SRGBColorSpace } from "three";
import { KeyboardControls, Stats } from "@react-three/drei";
import { useSceneContext } from "./sceneContext";
import { KeyboardControls } from "@react-three/drei";
import Builder from "../builder/builder";
import Visualization from "../visualization/visualization";
@@ -15,9 +14,12 @@ import useModuleStore from "../../store/useModuleStore";
import { useParams } from "react-router-dom";
import { getAllProjects } from "../../services/dashboard/getAllProjects";
import { getUserData } from "../../functions/getUserData";
import { useSceneContext } from "./sceneContext";
import { useLoadingProgress, useSocketStore } from "../../store/builder/store";
export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Comparison Layout' }) {
import StatsHelper from "./helpers/StatsHelper";
export default function Scene({ layout }: { readonly layout: "Main Layout" | "Comparison Layout"; }) {
const map = useMemo(() => [
{ name: "forward", keys: ["ArrowUp", "w", "W"] },
{ name: "backward", keys: ["ArrowDown", "s", "S"] },
@@ -34,28 +36,27 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
useEffect(() => {
if (!projectId && loadingProgress > 1) return;
getAllProjects(userId, organization)
.then((projects) => {
if (!projects || !projects.Projects) return;
let project = projects.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId);
const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName('canvas')[0];
if (!canvas) return;
const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png");
const updateProjects = {
projectId: project?.projectUuid,
organization,
userId,
projectName: project?.projectName,
thumbnail: screenshotDataUrl,
};
if (projectSocket) {
projectSocket.emit("v1:project:update", updateProjects);
}
}).catch((err) => {
console.error(err);
});
// eslint-disable-next-line
}, [activeModule, assets, loadingProgress])
getAllProjects(userId, organization).then((projects) => {
if (!projects || !projects.Projects) return;
let project = projects.Projects.find((val: any) => val.projectUuid === projectId || val._id === projectId);
const canvas = document.getElementById("sceneCanvas")?.getElementsByTagName("canvas")[0];
if (!canvas) return;
const screenshotDataUrl = (canvas as HTMLCanvasElement)?.toDataURL("image/png");
const updateProjects = {
projectId: project?._id,
organization,
userId,
projectName: project?.projectName,
thumbnail: screenshotDataUrl,
};
if (projectSocket) {
projectSocket.emit("v1:project:update", updateProjects);
}
}).catch((err) => {
console.error(err);
});
// eslint-disable-next-line
}, [activeModule, assets, loadingProgress]);
return (
<KeyboardControls map={map}>
@@ -64,14 +65,10 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
shadows
color="#aaaa"
eventPrefix="client"
onContextMenu={(e) => {
e.preventDefault();
}}
onContextMenu={(e) => { e.preventDefault(); }}
performance={{ min: 0.9, max: 1.0 }}
onCreated={(e) => {
e.scene.background = layout === 'Main Layout' ? null : new Color(0x19191d);
}}
gl={{ outputColorSpace: SRGBColorSpace, powerPreference: "high-performance", antialias: true, preserveDrawingBuffer: true }}
onCreated={(e) => { e.scene.background = layout === "Main Layout" ? null : new Color(0x19191d); }}
gl={{ outputColorSpace: SRGBColorSpace, powerPreference: "high-performance", antialias: true }}
>
<Setup />
<Collaboration />
@@ -84,7 +81,7 @@ export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Co
<PhysicsSimulator />
</Physics>
<Visualization />
<Stats />
<StatsHelper />
</Canvas>
</KeyboardControls>
);

View File

@@ -8,6 +8,7 @@ import { createZoneStore, ZoneStoreType } from '../../store/builder/useZoneStore
import { createFloorStore, FloorStoreType } from '../../store/builder/useFloorStore';
import { createUndoRedo2DStore, UndoRedo2DStoreType } from '../../store/builder/useUndoRedo2DStore';
import { createUndoRedo3DStore, UndoRedo3DStoreType } from '../../store/builder/useUndoRedo3DStore';
import { createEventStore, EventStoreType } from '../../store/simulation/useEventsStore';
import { createProductStore, ProductStoreType } from '../../store/simulation/useProductStore';
@@ -19,6 +20,7 @@ import { createConveyorStore, ConveyorStoreType } from '../../store/simulation/u
import { createVehicleStore, VehicleStoreType } from '../../store/simulation/useVehicleStore';
import { createStorageUnitStore, StorageUnitStoreType } from '../../store/simulation/useStorageUnitStore';
import { createHumanStore, HumanStoreType } from '../../store/simulation/useHumanStore';
import { createCraneStore, CraneStoreType } from '../../store/simulation/useCraneStore';
import { createColliderStore, ColliderStoreType } from '../../store/useColliderStore';
@@ -32,6 +34,7 @@ type SceneContextValue = {
floorStore: FloorStoreType,
undoRedo2DStore: UndoRedo2DStoreType,
undoRedo3DStore: UndoRedo3DStoreType,
eventStore: EventStoreType,
productStore: ProductStoreType,
@@ -43,10 +46,12 @@ type SceneContextValue = {
vehicleStore: VehicleStoreType;
storageUnitStore: StorageUnitStoreType;
humanStore: HumanStoreType;
craneStore: CraneStoreType;
colliderStore: ColliderStoreType;
humanEventManagerRef: React.RefObject<HumanEventManagerState>;
craneEventManagerRef: React.RefObject<CraneEventManagerState>;
clearStores: () => void;
@@ -71,6 +76,7 @@ export function SceneProvider({
const floorStore = useMemo(() => createFloorStore(), []);
const undoRedo2DStore = useMemo(() => createUndoRedo2DStore(), []);
const undoRedo3DStore = useMemo(() => createUndoRedo3DStore(), []);
const eventStore = useMemo(() => createEventStore(), []);
const productStore = useMemo(() => createProductStore(), []);
@@ -82,10 +88,12 @@ export function SceneProvider({
const vehicleStore = useMemo(() => createVehicleStore(), []);
const storageUnitStore = useMemo(() => createStorageUnitStore(), []);
const humanStore = useMemo(() => createHumanStore(), []);
const craneStore = useMemo(() => createCraneStore(), []);
const colliderStore = useMemo(() => createColliderStore(), []);
const humanEventManagerRef = useRef<HumanEventManagerState>({ humanStates: [] });
const craneEventManagerRef = useRef<CraneEventManagerState>({ craneStates: [] });
const clearStores = useMemo(() => () => {
assetStore.getState().clearAssets();
@@ -95,6 +103,7 @@ export function SceneProvider({
zoneStore.getState().clearZones();
floorStore.getState().clearFloors();
undoRedo2DStore.getState().clearUndoRedo2D();
undoRedo3DStore.getState().clearUndoRedo3D();
eventStore.getState().clearEvents();
productStore.getState().clearProducts();
materialStore.getState().clearMaterials();
@@ -104,9 +113,11 @@ export function SceneProvider({
vehicleStore.getState().clearVehicles();
storageUnitStore.getState().clearStorageUnits();
humanStore.getState().clearHumans();
colliderStore.getState().clearColliders();
craneStore.getState().clearCranes();
humanEventManagerRef.current.humanStates = [];
}, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, colliderStore]);
craneEventManagerRef.current.craneStates = [];
colliderStore.getState().clearColliders();
}, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, undoRedo3DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, colliderStore]);
const contextValue = useMemo(() => (
{
@@ -117,6 +128,7 @@ export function SceneProvider({
zoneStore,
floorStore,
undoRedo2DStore,
undoRedo3DStore,
eventStore,
productStore,
materialStore,
@@ -126,12 +138,14 @@ export function SceneProvider({
vehicleStore,
storageUnitStore,
humanStore,
colliderStore,
craneStore,
humanEventManagerRef,
craneEventManagerRef,
colliderStore,
clearStores,
layout
}
), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, colliderStore, clearStores, layout]);
), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, undoRedo3DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, colliderStore, clearStores, layout]);
return (
<SceneContext.Provider value={contextValue}>

View File

@@ -0,0 +1,37 @@
import { useCallback } from "react";
import { useSceneContext } from "../../../../scene/sceneContext";
import { useProductContext } from "../../../products/productContext";
export function usePickAndDropHandler() {
const { materialStore, craneStore, productStore } = useSceneContext();
const { getMaterialById } = materialStore();
const { getModelUuidByActionUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { incrementCraneLoad, addCurrentMaterial, addCurrentAction } = craneStore();
const pickAndDropLogStatus = (materialUuid: string, status: string) => {
echo.info(`${materialUuid}, ${status}`);
}
const handlePickAndDrop = useCallback((action: CraneAction, materialId?: string) => {
if (!action || action.actionType !== 'pickAndDrop' || !materialId) return;
const material = getMaterialById(materialId);
if (!material) return;
const modelUuid = getModelUuidByActionUuid(selectedProduct.productUuid, action.actionUuid);
if (!modelUuid) return;
incrementCraneLoad(modelUuid, 1);
addCurrentAction(modelUuid, action.actionUuid, material.materialType, materialId);
addCurrentMaterial(modelUuid, material.materialType, material.materialId);
pickAndDropLogStatus(material.materialName, `performing pickAndDrop action`);
}, [getMaterialById]);
return {
handlePickAndDrop,
};
}

View File

@@ -0,0 +1,36 @@
import { useEffect, useCallback } from 'react';
import { usePickAndDropHandler } from './actionHandler/usePickAndDropHandler';
export function useCraneActions() {
const { handlePickAndDrop } = usePickAndDropHandler();
const handleWorkerAction = useCallback((action: CraneAction, materialId: string) => {
handlePickAndDrop(action, materialId);
}, [handlePickAndDrop]);
const handleCraneAction = useCallback((action: CraneAction, materialId: string) => {
if (!action) return;
switch (action.actionType) {
case 'pickAndDrop':
handleWorkerAction(action, materialId);
break;
default:
console.warn(`Unknown Human action type: ${action.actionType}`);
}
}, [handleWorkerAction]);
const cleanup = useCallback(() => {
}, []);
useEffect(() => {
return () => {
cleanup();
};
}, [cleanup]);
return {
handleCraneAction,
cleanup
};
}

View File

@@ -6,13 +6,15 @@ import { useProductContext } from "../../../products/productContext";
import { useHumanEventManager } from "../../../human/eventManager/useHumanEventManager";
export function useRetrieveHandler() {
const { materialStore, armBotStore, machineStore, vehicleStore, storageUnitStore, productStore, humanStore, assetStore } = useSceneContext();
const { materialStore, armBotStore, machineStore, vehicleStore, storageUnitStore, conveyorStore, craneStore, productStore, humanStore, assetStore, humanEventManagerRef } = useSceneContext();
const { selectedProductStore } = useProductContext();
const { addMaterial } = materialStore();
const { getModelUuidByActionUuid, getPointUuidByActionUuid, getEventByModelUuid, getActionByUuid } = productStore();
const { getStorageUnitById, getLastMaterial, updateCurrentLoad, removeLastMaterial } = storageUnitStore();
const { getVehicleById, incrementVehicleLoad, addCurrentMaterial } = vehicleStore();
const { getConveyorById } = conveyorStore();
const { getHumanById, incrementHumanLoad, addCurrentMaterial: addCurrentMaterialToHuman } = humanStore();
const { getCraneById, incrementCraneLoad, addCurrentMaterial: addCurrentMaterialToCrane, addCurrentAction: addCurrentActionToCrane } = craneStore();
const { getAssetById, setCurrentAnimation } = assetStore();
const { selectedProduct } = selectedProductStore();
const { getArmBotById, addCurrentAction } = armBotStore();
@@ -27,6 +29,7 @@ export function useRetrieveHandler() {
const retrievalTimeRef = useRef<Map<string, number>>(new Map());
const retrievalCountRef = useRef<Map<string, number>>(new Map());
const monitoredHumansRef = useRef<Set<string>>(new Set());
const cranePickupLockRef = useRef<Map<string, boolean>>(new Map());
const [initialDelayComplete, setInitialDelayComplete] = useState(false);
const delayTimerRef = useRef<NodeJS.Timeout | null>(null);
@@ -60,9 +63,9 @@ export function useRetrieveHandler() {
actionUuid: action.actionUuid
},
current: {
modelUuid: action.triggers[0]?.triggeredAsset.triggeredModel.modelUuid,
pointUuid: action.triggers[0]?.triggeredAsset.triggeredPoint.pointUuid,
actionUuid: action.triggers[0]?.triggeredAsset.triggeredAction.actionUuid
modelUuid: modelUuid,
pointUuid: pointUuid,
actionUuid: action.actionUuid
},
};
@@ -371,6 +374,32 @@ export function useRetrieveHandler() {
}
return;
}
} else if (triggeredModel?.type === 'transfer') {
const model = getConveyorById(triggeredModel.modelUuid);
if (model && !model.isPaused) {
if (humanAsset?.animationState?.current === 'idle') {
setCurrentAnimation(human.modelUuid, 'pickup', true, false, false);
} else if (humanAsset?.animationState?.current === 'pickup' && humanAsset.animationState.isCompleted) {
const lastMaterial = getLastMaterial(storageUnit.modelUuid);
if (lastMaterial) {
const material = createNewMaterial(
lastMaterial.materialId,
lastMaterial.materialType,
storageUnit.point.action
);
if (material) {
removeLastMaterial(storageUnit.modelUuid);
updateCurrentLoad(storageUnit.modelUuid, -1);
incrementHumanLoad(human.modelUuid, 1);
addCurrentMaterialToHuman(human.modelUuid, material.materialType, material.materialId);
retrieveLogStatus(material.materialName, `is picked by ${human.modelName}`);
retrievalCountRef.current.set(actionUuid, currentCount + 1);
}
}
}
return;
}
} else if (triggeredModel?.type === 'machine') {
const machine = getMachineById(triggeredModel.modelUuid);
if (machine && !machine.isActive && machine.state === 'idle' && !machine.currentAction) {
@@ -423,7 +452,57 @@ export function useRetrieveHandler() {
}
}
}
} else if (triggeredModel?.type === 'crane') {
const crane = getCraneById(triggeredModel.modelUuid);
const action = getActionByUuid(selectedProduct.productUuid, crane?.currentAction?.actionUuid || '');
const currentCount = retrievalCountRef.current.get(actionUuid) ?? 0;
if (!crane) return;
const hasLock = cranePickupLockRef.current.get(crane.modelUuid) || false;
if (action && action.actionType === 'pickAndDrop' && !hasLock && !crane.isCarrying && !crane.isActive && crane.currentLoad < (action?.maxPickUpCount || 0)) {
const material = getLastMaterial(storageUnit.modelUuid);
if (material) {
if (action.triggers[0].triggeredAsset?.triggeredModel.modelUuid && action.triggers[0].triggeredAsset.triggeredAction?.actionUuid) {
const human = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0].triggeredAsset.triggeredModel.modelUuid);
if (human && human.type === 'human') {
if (!monitoredHumansRef.current.has(human.modelUuid)) {
addHumanToMonitor(human.modelUuid, () => {
incrementCraneLoad(crane.modelUuid, 1);
addCurrentActionToCrane(crane.modelUuid, action.actionUuid, material.materialType, material.materialId);
addCurrentMaterialToCrane(crane.modelUuid, material.materialType, material.materialId);
cranePickupLockRef.current.set(crane.modelUuid, true);
}, action.triggers[0].triggeredAsset.triggeredAction?.actionUuid)
}
monitoredHumansRef.current.add(human.modelUuid);
}
}
}
} else if (crane.isCarrying && crane.currentPhase === 'pickup-drop' && hasLock) {
cranePickupLockRef.current.set(crane.modelUuid, false);
const lastMaterial = getLastMaterial(storageUnit.modelUuid);
if (lastMaterial) {
const material = createNewMaterial(
lastMaterial.materialId,
lastMaterial.materialType,
storageUnit.point.action
);
if (material) {
removeLastMaterial(storageUnit.modelUuid);
updateCurrentLoad(storageUnit.modelUuid, -1);
retrievalCountRef.current.set(actionUuid, currentCount + 1);
}
}
} else if (!action) {
const action = getActionByUuid(selectedProduct.productUuid, retrieval.action.triggers[0]?.triggeredAsset.triggeredAction?.actionUuid || '');
if (action) {
addCurrentActionToCrane(crane.modelUuid, action.actionUuid, null, null);
}
}
}
});
if (hasChanges || completedActions.length > 0) {

View File

@@ -8,6 +8,7 @@ import { useRoboticArmActions } from "./roboticArm/useRoboticArmActions";
import { useStorageActions } from "./storageUnit/useStorageUnitActions";
import { useVehicleActions } from "./vehicle/useVehicleActions";
import { useHumanActions } from "./human/useHumanActions";
import { useCraneActions } from "./crane/useCraneActions";
import { useCallback, useEffect } from "react";
export function useActionHandler() {
@@ -19,6 +20,7 @@ export function useActionHandler() {
const { handleMachineAction, cleanup: cleanupMachine } = useMachineActions();
const { handleStorageAction, cleanup: cleanupStorage } = useStorageActions();
const { handleHumanAction, cleanup: cleanupHuman } = useHumanActions();
const { handleCraneAction, cleanup: cleanupCrane } = useCraneActions();
const handleAction = useCallback((action: Action, materialId?: string) => {
if (!action) return;
@@ -42,6 +44,9 @@ export function useActionHandler() {
case 'worker': case 'assembly':
handleHumanAction(action as HumanAction, materialId as string);
break;
case 'pickAndDrop':
handleCraneAction(action as CraneAction, materialId as string);
break;
default:
console.warn(`Unknown action type: ${(action as Action).actionType}`);
}
@@ -49,7 +54,7 @@ export function useActionHandler() {
echo.error("Failed to handle action");
console.error("Error handling action:", error);
}
}, [handleConveyorAction, handleVehicleAction, handleRoboticArmAction, handleMachineAction, handleStorageAction, handleHumanAction]);
}, [handleConveyorAction, handleVehicleAction, handleRoboticArmAction, handleMachineAction, handleStorageAction, handleHumanAction, handleCraneAction]);
const cleanup = useCallback(() => {
cleanupConveyor();
@@ -58,7 +63,8 @@ export function useActionHandler() {
cleanupMachine();
cleanupStorage();
cleanupHuman();
}, [cleanupConveyor, cleanupVehicle, cleanupRoboticArm, cleanupMachine, cleanupStorage, cleanupHuman]);
cleanupCrane();
}, [cleanupConveyor, cleanupVehicle, cleanupRoboticArm, cleanupMachine, cleanupStorage, cleanupHuman, cleanupCrane]);
useEffect(() => {
return () => {

View File

@@ -0,0 +1,15 @@
import React from 'react'
import CraneInstances from './instances/craneInstances'
function Crane() {
return (
<>
<CraneInstances />
</>
)
}
export default Crane

View File

@@ -0,0 +1,170 @@
import { useEffect } from 'react';
import { useFrame } from '@react-three/fiber';
import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../scene/sceneContext';
import { useProductContext } from '../../products/productContext';
export function useCraneEventManager() {
const { craneStore, productStore, assetStore, vehicleStore, armBotStore, machineStore, craneEventManagerRef } = useSceneContext();
const { getCraneById, setCurrentPhase, removeCurrentAction } = craneStore();
const { getAssetById } = assetStore();
const { getVehicleById } = vehicleStore();
const { getArmBotById } = armBotStore();
const { getMachineById } = machineStore();
const { getActionByUuid, getEventByModelUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { isPlaying } = usePlayButtonStore();
const { isPaused } = usePauseButtonStore();
const { isReset } = useResetButtonStore();
useEffect(() => {
if ((isReset || !isPlaying) && craneEventManagerRef.current) {
craneEventManagerRef.current.craneStates = [];
}
}, [isReset, isPlaying]);
const addCraneToMonitor = (craneId: string, callback: () => void, actionUuid: string) => {
const crane = getCraneById(craneId);
const action = getActionByUuid(selectedProduct.productUuid, actionUuid) as CraneAction | undefined;
if (!crane || !action || action.actionType !== 'pickAndDrop' || !craneEventManagerRef.current) return;
let state = craneEventManagerRef.current.craneStates.find(c => c.craneId === craneId);
if (!state) {
state = {
craneId,
pendingActions: [],
currentAction: null,
isProcessing: false
};
craneEventManagerRef.current.craneStates.push(state);
}
state.pendingActions.push({
actionUuid,
callback
});
if (!state.isProcessing) {
processNextAction(state);
}
};
const processNextAction = (state: CraneEventState) => {
if (state.pendingActions.length === 0) {
state.currentAction = null;
return;
}
state.isProcessing = true;
state.currentAction = state.pendingActions.shift() || null;
};
const completeCurrentAction = (state: CraneEventState) => {
processNextAction(state);
};
useFrame(() => {
if (!craneEventManagerRef.current || craneEventManagerRef.current.craneStates.length === 0 || !isPlaying || isPaused) return;
for (const craneState of craneEventManagerRef.current.craneStates) {
if (!craneState.currentAction || !craneState.isProcessing) continue;
const { craneId, currentAction } = craneState;
const crane = getCraneById(craneId);
const craneAsset = getAssetById(craneId);
const currentCraneAction = getActionByUuid(selectedProduct.productUuid, crane?.currentAction?.actionUuid || '') as CraneAction | undefined;
if (!crane || !craneAsset || !currentCraneAction) continue;
if (crane.isActive || crane.state !== "idle") continue;
if (currentCraneAction.actionType === 'pickAndDrop' && crane.currentLoad < currentCraneAction.maxPickUpCount) {
const humanAction = getActionByUuid(selectedProduct.productUuid, currentCraneAction.triggers[0].triggeredAsset?.triggeredAction?.actionUuid || '');
if (humanAction) {
const nextEvent = getEventByModelUuid(selectedProduct.productUuid, humanAction.triggers[0].triggeredAsset?.triggeredModel?.modelUuid || '')
if (nextEvent) {
if (nextEvent.type === 'transfer') {
if (currentAction.actionUuid !== crane.currentAction?.actionUuid) {
setCurrentPhase(crane.modelUuid, 'init');
removeCurrentAction(crane.modelUuid);
}
craneState.isProcessing = false;
currentAction.callback();
setTimeout(() => {
completeCurrentAction(craneState);
}, 1000);
} else if (nextEvent.type === 'vehicle') {
const vehicle = getVehicleById(nextEvent.modelUuid);
if (vehicle && !vehicle.isActive && vehicle.currentPhase === 'picking') {
if (currentAction.actionUuid !== crane.currentAction?.actionUuid) {
setCurrentPhase(crane.modelUuid, 'init');
removeCurrentAction(crane.modelUuid);
}
craneState.isProcessing = false;
currentAction.callback();
setTimeout(() => {
completeCurrentAction(craneState);
}, 1000);
}
} else if (nextEvent.type === 'roboticArm') {
const armBot = getArmBotById(nextEvent.modelUuid);
if (armBot && !armBot.isActive) {
if (currentAction.actionUuid !== crane.currentAction?.actionUuid) {
setCurrentPhase(crane.modelUuid, 'init');
removeCurrentAction(crane.modelUuid);
}
craneState.isProcessing = false;
currentAction.callback();
setTimeout(() => {
completeCurrentAction(craneState);
}, 1000);
}
} else if (nextEvent.type === 'machine') {
const machine = getMachineById(nextEvent.modelUuid);
if (machine && !machine.isActive) {
if (currentAction.actionUuid !== crane.currentAction?.actionUuid) {
setCurrentPhase(crane.modelUuid, 'init');
removeCurrentAction(crane.modelUuid);
}
craneState.isProcessing = false;
currentAction.callback();
setTimeout(() => {
completeCurrentAction(craneState);
}, 1000);
}
} else {
if (currentAction.actionUuid !== crane.currentAction?.actionUuid) {
setCurrentPhase(crane.modelUuid, 'init');
removeCurrentAction(crane.modelUuid);
}
craneState.isProcessing = false;
currentAction.callback();
setTimeout(() => {
completeCurrentAction(craneState);
}, 1000);
}
}
}
}
}
}, 0);
return {
addCraneToMonitor
};
}

View File

@@ -0,0 +1,57 @@
import { useFrame, useThree } from '@react-three/fiber';
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { MaterialModel } from '../../../materials/instances/material/materialModel';
type MaterialAnimatorProps = {
crane: CraneStatus;
};
export default function MaterialAnimator({ crane }: Readonly<MaterialAnimatorProps>) {
const materialRef = useRef<any>(null);
const { scene } = useThree();
const [isRendered, setIsRendered] = useState<boolean>(false);
useEffect(() => {
if (crane.isCarrying) {
setIsRendered(true);
} else {
setIsRendered(false);
}
}, [crane.isCarrying]);
useFrame(() => {
const craneModel = scene.getObjectByProperty('uuid', crane.modelUuid);
if (!materialRef.current || !craneModel) return;
const base = craneModel.getObjectByName('hook');
if (!base) return;
if (crane.isCarrying) {
const boneWorldPos = new THREE.Vector3();
base.getWorldPosition(boneWorldPos);
const yOffset = -0.65;
boneWorldPos.y += yOffset;
materialRef.current.position.copy(boneWorldPos);
materialRef.current.up.set(0, 1, 0);
materialRef.current.lookAt(
materialRef.current.position.clone().add(new THREE.Vector3(0, 0, 1))
);
}
});
return (
<>
{isRendered && (
<MaterialModel
materialId={crane.currentAction?.materialId ?? ''}
matRef={materialRef}
materialType={crane.currentAction?.materialType ?? 'Default material'}
/>
)}
</>
);
}

View File

@@ -0,0 +1,375 @@
import { useEffect, useState } from 'react';
import * as THREE from 'three';
import { useFrame, useThree } from '@react-three/fiber';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../products/productContext';
import { dragAction } from '@use-gesture/react';
function PillarJibAnimator({
crane,
points,
setPoints,
animationPhase,
setAnimationPhase,
onAnimationComplete
}: {
crane: CraneStatus;
points: [THREE.Vector3, THREE.Vector3] | null;
setPoints: (points: [THREE.Vector3, THREE.Vector3] | null) => void;
animationPhase: string;
setAnimationPhase: (phase: string) => void;
onAnimationComplete: (action: string) => void;
}) {
const { scene } = useThree();
const { assetStore, productStore, materialStore } = useSceneContext();
const { getActionByUuid, getPointByUuid } = productStore();
const { resetAsset } = assetStore();
const { setIsVisible } = materialStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { isPaused } = usePauseButtonStore();
const { isPlaying } = usePlayButtonStore();
const { isReset } = useResetButtonStore();
const { speed } = useAnimationPlaySpeed();
const [clampedPoints, setClampedPoints] = useState<[THREE.Vector3, THREE.Vector3]>();
useEffect(() => {
if (!isPlaying || isReset) {
resetAsset(crane.modelUuid);
setAnimationPhase('idle');
setPoints(null);
}
}, [isPlaying, scene, crane.modelUuid, isReset]);
useEffect(() => {
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
if (!model) return;
const hook = model.getObjectByName('hook');
if (!hook) return;
const hookWorld = new THREE.Vector3();
hook.getWorldPosition(hookWorld);
if (crane.currentPhase === 'init-pickup') {
if (crane.currentMaterials.length > 0) {
const materials = scene.getObjectsByProperty('uuid', crane.currentMaterials[0].materialId);
console.log('materials: ', materials);
const material = materials.find((material) => material.visible === true);
if (material) {
const materialWorld = new THREE.Vector3();
material.getWorldPosition(materialWorld);
setAnimationPhase('init-hook-adjust');
setPoints(
[
new THREE.Vector3(hookWorld.x, hookWorld.y, hookWorld.z),
new THREE.Vector3(materialWorld.x, materialWorld.y + 0.5, materialWorld.z)
]
);
}
}
} else if (crane.currentPhase === 'pickup-drop') {
if (crane.currentMaterials.length > 0) {
const action = getActionByUuid(selectedProduct.productUuid, crane?.currentAction?.actionUuid || '');
const humanAction = getActionByUuid(selectedProduct.productUuid, action?.triggers[0].triggeredAsset?.triggeredAction?.actionUuid || '');
if (humanAction) {
const point = getPointByUuid(selectedProduct.productUuid, humanAction.triggers[0].triggeredAsset?.triggeredModel?.modelUuid || '', humanAction.triggers[0].triggeredAsset?.triggeredPoint?.pointUuid || '');
const eventModel = scene.getObjectByProperty('uuid', humanAction.triggers[0].triggeredAsset?.triggeredModel?.modelUuid);
if (point && eventModel) {
const pointLocal = new THREE.Vector3(...point.position);
const pointWorld = pointLocal.clone().applyMatrix4(eventModel.matrixWorld);
const startPoint = new THREE.Vector3(hookWorld.x, hookWorld.y, hookWorld.z);
const endPoint = new THREE.Vector3(pointWorld.x, pointWorld.y + 0.5, pointWorld.z);
setIsVisible(crane.currentMaterials[0].materialId, false);
setAnimationPhase('init-hook-adjust');
setPoints([startPoint, endPoint]);
}
}
}
}
}, [crane.currentPhase])
useEffect(() => {
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
if (!model || !model.userData.fieldData) return;
const base = model.getObjectByName('base');
const trolley = model.getObjectByName('trolley');
const hook = model.getObjectByName('hook');
if (!base || !trolley || !hook || !points) return;
const baseWorld = new THREE.Vector3();
base.getWorldPosition(baseWorld);
const trolleyWorld = new THREE.Vector3();
trolley.getWorldPosition(trolleyWorld);
const hookWorld = new THREE.Vector3();
hook.getWorldPosition(hookWorld);
const trolleyMinOffset = model.userData.trolleyMinOffset || -1;
const trolleyMaxOffset = model.userData.trolleyMaxOffset || 1.75;
const hookMinOffset = model.userData.hookMinOffset || 0.25;
const hookMaxOffset = model.userData.hookMaxOffset || -1.5;
const distFromBase = new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length();
const innerRadius = Math.max(distFromBase + trolleyMinOffset, 0.05);
const outerRadius = Math.max(
new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length() + trolleyMaxOffset,
innerRadius
);
const yMin = hookWorld.y + hookMaxOffset;
const yMax = hookWorld.y + hookMinOffset;
function clampToCylinder(pos: THREE.Vector3) {
const xzDist = new THREE.Vector2(pos.x - baseWorld.x, pos.z - baseWorld.z);
const distance = xzDist.length();
let clampedDistance = THREE.MathUtils.clamp(distance, innerRadius, outerRadius);
if (distance > 0) {
clampedDistance = Math.max(innerRadius, Math.min(distance, outerRadius));
}
const clampedXZ = xzDist.normalize().multiplyScalar(clampedDistance);
const y = THREE.MathUtils.clamp(pos.y, yMin, yMax);
return new THREE.Vector3(baseWorld.x + clampedXZ.x, y, baseWorld.z + clampedXZ.y);
}
const newClampedPoints: [THREE.Vector3, THREE.Vector3] = [new THREE.Vector3(), new THREE.Vector3()];
const newIsInside: [boolean, boolean] = [false, false];
points.forEach((point, i) => {
const xzDist = new THREE.Vector2(point.x - baseWorld.x, point.z - baseWorld.z).length();
const insideXZ = xzDist >= innerRadius && xzDist <= outerRadius;
const insideY = point.y >= yMin && point.y <= yMax;
newIsInside[i] = insideXZ && insideY;
newClampedPoints[i] = newIsInside[i] ? point.clone() : clampToCylinder(point);
});
setClampedPoints(newClampedPoints);
}, [crane.modelUuid, points]);
useFrame(() => {
if (!isPlaying || isPaused || !points || !clampedPoints || animationPhase === 'idle') return;
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
if (!model) return;
const base = model.getObjectByName('base');
const trolley = model.getObjectByName('trolley');
const hook = model.getObjectByName('hook');
if (!base || !trolley || !hook || !trolley.parent) return;
const baseWorld = new THREE.Vector3();
base.getWorldPosition(baseWorld);
const hookWorld = new THREE.Vector3();
hook.getWorldPosition(hookWorld);
if (!model.userData.animationData) {
model.userData.animationData = {
originalHookY: hook.position.y,
targetHookY: clampedPoints[0].y - baseWorld.y,
targetDirection: new THREE.Vector2(),
targetTrolleyX: 0,
targetWorldPosition: clampedPoints[0].clone(),
finalHookTargetY: 0,
};
}
const { animationData } = model.userData;
const hookSpeed = (model.userData.hookSpeed || 0.01) * speed;
const rotationSpeed = (model.userData.rotationSpeed || 0.005) * speed;
const trolleySpeed = (model.userData.trolleySpeed || 0.01) * speed;
const threshold = Math.max(0.01, 0.05 / speed);
switch (animationPhase) {
case 'init-hook-adjust': {
const hookWorld = new THREE.Vector3();
hook.getWorldPosition(hookWorld);
const targetY = clampedPoints[0].y;
const direction = Math.sign(targetY - hookWorld.y);
hook.position.y = THREE.MathUtils.lerp(
hook.position.y,
hook.position.y + direction * 0.1,
Math.min(hookSpeed, 0.9)
);
if (Math.abs(hookWorld.y - targetY) < threshold) {
hook.position.y = targetY - baseWorld.y;
setAnimationPhase('init-rotate-base');
}
break;
}
case 'init-rotate-base': {
const baseForward = new THREE.Vector3(1, 0, 0);
base.localToWorld(baseForward);
baseForward.sub(baseWorld);
const currentDir = new THREE.Vector2(baseForward.x, baseForward.z).normalize();
const targetWorld = clampedPoints[0];
const targetDir = new THREE.Vector2(
targetWorld.x - baseWorld.x,
targetWorld.z - baseWorld.z
).normalize();
const currentAngle = Math.atan2(currentDir.y, currentDir.x);
const targetAngle = Math.atan2(targetDir.y, targetDir.x);
let angleDiff = currentAngle - targetAngle;
angleDiff = Math.atan2(Math.sin(angleDiff), Math.cos(angleDiff));
if (Math.abs(angleDiff) > threshold) {
base.rotation.y = THREE.MathUtils.lerp(
base.rotation.y,
base.rotation.y - angleDiff,
Math.min(rotationSpeed, 0.9)
);
} else {
base.rotation.y -= angleDiff;
const localTarget = trolley.parent.worldToLocal(clampedPoints[0].clone());
animationData.targetTrolleyX = localTarget?.x;
setAnimationPhase('init-move-trolley');
}
break;
}
case 'init-move-trolley': {
const dx = animationData.targetTrolleyX - trolley.position.x;
trolley.position.x = THREE.MathUtils.lerp(
trolley.position.x,
animationData.targetTrolleyX,
Math.min(trolleySpeed, 0.9)
);
if (Math.abs(dx) < threshold) {
trolley.position.x = animationData.targetTrolleyX;
animationData.finalHookTargetY = clampedPoints[0].y - baseWorld.y;
setAnimationPhase('init-final-hook-adjust');
}
break;
}
case 'init-final-hook-adjust': {
const targetY = clampedPoints[0].y - baseWorld.y;
hook.position.y = THREE.MathUtils.lerp(
hook.position.y,
targetY,
Math.min(hookSpeed, 0.9)
);
if (Math.abs(hook.position.y - targetY) < threshold) {
hook.position.y = targetY;
model.userData.animationData = {
originalHookY: hook.position.y,
targetHookY: clampedPoints[1].y - baseWorld.y + 0.5,
targetDirection: new THREE.Vector2(),
targetTrolleyX: 0,
targetWorldPosition: clampedPoints[1].clone(),
finalHookTargetY: 0,
};
setAnimationPhase('starting');
onAnimationComplete('starting');
}
break;
}
case 'first-hook-adjust': {
const direction = Math.sign(animationData.targetHookY - hook.position.y);
hook.position.y += direction * hookSpeed;
if (parseFloat(Math.abs(hook.position.y - animationData.targetHookY).toFixed(2)) < 0.05) {
hook.position.y = animationData.targetHookY;
setAnimationPhase('first-rotate-base');
}
break;
}
case 'first-rotate-base': {
const baseForward = new THREE.Vector3(1, 0, 0);
base.localToWorld(baseForward);
baseForward.sub(baseWorld);
const currentDir = new THREE.Vector2(baseForward.x, baseForward.z).normalize();
const targetWorld = clampedPoints[1];
const targetDir = new THREE.Vector2(
targetWorld.x - baseWorld.x,
targetWorld.z - baseWorld.z
).normalize();
const currentAngle = Math.atan2(currentDir.y, currentDir.x);
const targetAngle = Math.atan2(targetDir.y, targetDir.x);
let angleDiff = currentAngle - targetAngle;
angleDiff = Math.atan2(Math.sin(angleDiff), Math.cos(angleDiff));
if (parseFloat(Math.abs(angleDiff).toFixed(2)) > 0.05) {
base.rotation.y += Math.sign(angleDiff) * rotationSpeed;
} else {
base.rotation.y += angleDiff;
const localTarget = trolley.parent.worldToLocal(clampedPoints[1].clone());
animationData.targetTrolleyX = localTarget?.x;
setAnimationPhase('first-move-trolley');
}
break;
}
case 'first-move-trolley': {
const dx = animationData.targetTrolleyX - trolley.position.x;
const direction = Math.sign(dx);
trolley.position.x += direction * trolleySpeed;
if (parseFloat(Math.abs(dx).toFixed(2)) < 0.05) {
trolley.position.x = animationData.targetTrolleyX;
animationData.finalHookTargetY = hook.position.y - 0.5;
setAnimationPhase('first-final-hook-adjust');
}
break;
}
case 'first-final-hook-adjust': {
const dy = animationData.finalHookTargetY - hook.position.y;
const direction = Math.sign(dy);
hook.position.y += direction * hookSpeed;
if (parseFloat(Math.abs(dy).toFixed(2)) < 0.05) {
hook.position.y = animationData.finalHookTargetY;
if (crane.currentPhase === 'init-pickup') {
setAnimationPhase('picking');
onAnimationComplete('picking');
} else if (crane.currentPhase === 'pickup-drop') {
setAnimationPhase('dropping');
onAnimationComplete('dropping');
}
}
break;
}
}
});
return (
<>
</>
);
}
export default PillarJibAnimator;

View File

@@ -0,0 +1,24 @@
import React from 'react'
import PillarJibInstance from './instance/pillarJibInstance';
import { useSceneContext } from '../../../scene/sceneContext';
function CraneInstances() {
const { craneStore } = useSceneContext();
const { cranes } = craneStore();
return (
<>
{cranes.map((crane: CraneStatus) => (
<React.Fragment key={crane.modelUuid}>
{crane.subType === "pillarJib" &&
<PillarJibInstance key={crane.modelUuid} crane={crane} />
}
</React.Fragment>
))}
</>
)
}
export default CraneInstances

View File

@@ -0,0 +1,171 @@
import { useEffect, useMemo, useRef, useState } from "react";
import * as THREE from "three";
import { useThree } from "@react-three/fiber";
import { Box, Sphere } from "@react-three/drei";
function PillarJibHelper({
crane,
points,
isHelperNeeded
}: {
crane: CraneStatus,
points: [THREE.Vector3, THREE.Vector3] | null;
isHelperNeeded: boolean;
}) {
const { scene } = useThree();
const [clampedPoints, setClampedPoints] = useState<[THREE.Vector3, THREE.Vector3]>();
const [isInside, setIsInside] = useState<[boolean, boolean]>([false, false]);
const baseWorldRef = useRef<THREE.Vector3 | null>(null);
const trolleyWorldRef = useRef<THREE.Vector3 | null>(null);
const hookWorldRef = useRef<THREE.Vector3 | null>(null);
useEffect(() => {
const model = scene.getObjectByProperty("uuid", crane.modelUuid);
if (!model) return;
const base = model.getObjectByName("base");
const trolley = model.getObjectByName("trolley");
const hook = model.getObjectByName("hook");
if (!base || !trolley || !hook) return;
const bw = new THREE.Vector3();
const tw = new THREE.Vector3();
const hw = new THREE.Vector3();
base.getWorldPosition(bw);
trolley.getWorldPosition(tw);
hook.getWorldPosition(hw);
baseWorldRef.current = bw;
trolleyWorldRef.current = tw;
hookWorldRef.current = hw;
}, [scene, crane.modelUuid]);
const { geometry, position } = useMemo(() => {
const model = scene.getObjectByProperty('uuid', crane.modelUuid);
if (!model || !baseWorldRef.current || !trolleyWorldRef.current || !hookWorldRef.current) return { geometry: null, position: null };
const base = model.getObjectByName('base');
const trolley = model.getObjectByName('trolley');
const hook = model.getObjectByName('hook');
if (!base || !trolley || !hook || !points) return { geometry: null, position: null };
const baseWorld = baseWorldRef.current;
const trolleyWorld = trolleyWorldRef.current;
const hookWorld = hookWorldRef.current;
const trolleyMinOffset = model.userData.trolleyMinOffset || -1;
const trolleyMaxOffset = model.userData.trolleyMaxOffset || 1.75;
const hookMinOffset = model.userData.hookMinOffset || 0.25;
const hookMaxOffset = model.userData.hookMaxOffset || -1.5;
const distFromBase = new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length();
const outerRadius = distFromBase + trolleyMaxOffset;
const innerRadius = Math.max(distFromBase + trolleyMinOffset, 0.05);
const height = (hookMinOffset - (hookMaxOffset));
const cylinderYPosition = hookWorld.y + (height / 2) + (hookMaxOffset + hookMinOffset) / 2;
const shape = new THREE.Shape();
shape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false);
const hole = new THREE.Path();
hole.absarc(0, 0, innerRadius, 0, Math.PI * 2, true);
shape.holes.push(hole);
const extrudeSettings = {
depth: height,
bevelEnabled: false,
steps: 1
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const position: [number, number, number] = [baseWorld.x, cylinderYPosition, baseWorld.z];
const yMin = hookWorld.y + hookMaxOffset;
const yMax = hookWorld.y + hookMinOffset;
function clampToCylinder(pos: THREE.Vector3) {
const xzDist = new THREE.Vector2(pos.x - baseWorld.x, pos.z - baseWorld.z);
const distance = xzDist.length();
let clampedDistance = THREE.MathUtils.clamp(distance, innerRadius, outerRadius);
if (distance > 0) {
clampedDistance = Math.max(innerRadius, Math.min(distance, outerRadius));
}
const clampedXZ = xzDist.normalize().multiplyScalar(clampedDistance);
const y = THREE.MathUtils.clamp(pos.y, yMin, yMax);
return new THREE.Vector3(baseWorld.x + clampedXZ.x, y, baseWorld.z + clampedXZ.y);
}
const newClampedPoints: [THREE.Vector3, THREE.Vector3] = [new THREE.Vector3(), new THREE.Vector3()];
const newIsInside: [boolean, boolean] = [false, false];
points.forEach((point, i) => {
const xzDist = new THREE.Vector2(point.x - baseWorld.x, point.z - baseWorld.z).length();
const insideXZ = xzDist >= innerRadius && xzDist <= outerRadius;
const insideY = point.y >= yMin && point.y <= yMax;
newIsInside[i] = insideXZ && insideY;
newClampedPoints[i] = newIsInside[i] ? point.clone() : clampToCylinder(point);
});
setClampedPoints(newClampedPoints);
setIsInside(newIsInside);
return { geometry, position };
}, [scene, crane.modelUuid, points]);
if (!geometry || !position) return null;
return (
<>
{isHelperNeeded &&
<>
<mesh
geometry={geometry}
position={position}
rotation={[Math.PI / 2, 0, 0]}
>
<meshStandardMaterial
color={0x888888}
metalness={0.5}
roughness={0.4}
side={THREE.DoubleSide}
transparent={true}
opacity={0.3}
/>
</mesh>
{points && points.map((point, i) => (
<Box
key={`original-${i}`}
position={point}
args={[0.15, 0.15, 0.15]}
>
<meshStandardMaterial color="yellow" />
</Box>
))}
{clampedPoints && clampedPoints.map((point, i) => (
<Sphere
key={`clamped-${i}`}
position={point}
args={[0.1, 16, 16]}
>
<meshStandardMaterial color={isInside[i] ? 'lightgreen' : 'orange'} />
</Sphere>
))}
</>
}
</>
);
}
export default PillarJibHelper;

View File

@@ -0,0 +1,114 @@
import { useEffect, useState } from 'react';
import * as THREE from 'three'
import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../products/productContext';
import { useTriggerHandler } from '../../../triggers/triggerHandler/useTriggerHandler';
import PillarJibAnimator from '../animator/pillarJibAnimator'
import PillarJibHelper from '../helper/pillarJibHelper'
import MaterialAnimator from '../animator/materialAnimator';
function PillarJibInstance({ crane }: { crane: CraneStatus }) {
const { isPlaying } = usePlayButtonStore();
const { craneStore, productStore, humanStore, assetStore } = useSceneContext();
const { triggerPointActions } = useTriggerHandler();
const { getActionByUuid } = productStore();
const { setCurrentPhase, setCraneActive, setIsCaryying, removeCurrentAction, removeLastMaterial, decrementCraneLoad } = craneStore();
const { setCurrentPhase: setCurrentPhaseHuman, setHumanActive, setHumanState, getHumanById } = humanStore();
const { setCurrentAnimation, getAssetById } = assetStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const [animationPhase, setAnimationPhase] = useState<string>('idle');
const [points, setPoints] = useState<[THREE.Vector3, THREE.Vector3] | null>(null);
const action = getActionByUuid(selectedProduct.productUuid, crane?.currentAction?.actionUuid || '');
const actionTriggers = action?.triggers || [];
const humanId = actionTriggers?.[0]?.triggeredAsset?.triggeredModel?.modelUuid ?? null;
const humanAsset = getAssetById(humanId || '');
const humanAction = getActionByUuid(selectedProduct.productUuid, actionTriggers?.[0]?.triggeredAsset?.triggeredAction?.actionUuid ?? '');
useEffect(() => {
if (isPlaying) {
const human = getHumanById(humanId || '');
if (!human || !humanAsset || !humanId || !action || action.actionType !== 'pickAndDrop') return;
if (!crane.isActive && crane.currentPhase === 'init' && crane.currentMaterials.length > 0 && action.maxPickUpCount <= crane.currentMaterials.length) {
setCurrentPhase(crane.modelUuid, 'init-pickup');
} else if (crane.currentPhase === 'picking' && crane.currentMaterials.length > 0 && action.maxPickUpCount <= crane.currentMaterials.length && !crane.isCarrying) {
if (action.triggers.length > 0) {
if (humanAsset?.animationState?.current === "working_standing" && humanAsset?.animationState?.isCompleted && humanId && humanAction && humanAction.actionType === 'operator') {
setCurrentAnimation(humanId, 'idle', true, true, true);
setIsCaryying(crane.modelUuid, true);
setCurrentPhase(crane.modelUuid, 'pickup-drop');
} else {
setCurrentPhaseHuman(humanId, 'hooking');
setHumanActive(humanId, true);
setHumanState(humanId, 'running');
setCurrentAnimation(humanId, 'working_standing', true, false, false);
}
}
} else if (crane.currentPhase === 'dropping' && crane.currentMaterials.length > 0 && action.maxPickUpCount <= crane.currentMaterials.length && crane.isCarrying && human.currentPhase === 'hooking') {
setCurrentPhaseHuman(humanId, 'loadPoint-unloadPoint');
} else if (human.state === 'running' && human.currentPhase === 'unhooking') {
if (humanAsset?.animationState?.current === "working_standing" && humanAsset?.animationState?.isCompleted) {
setCurrentPhase(crane.modelUuid, 'init');
setCraneActive(crane.modelUuid, false);
setCurrentAnimation(humanId, 'idle', true, true, true);
setCurrentPhaseHuman(humanId, 'init');
setHumanActive(humanId, false);
setHumanState(humanId, 'idle');
handleMaterialDrop();
}
}
}
}, [crane, humanAsset?.animationState?.isCompleted])
const handleMaterialDrop = () => {
if (humanAction && humanAction.actionType === 'operator') {
setIsCaryying(crane.modelUuid, false);
removeCurrentAction(crane.modelUuid);
const removedMaterial = removeLastMaterial(crane.modelUuid);
decrementCraneLoad(crane.modelUuid, 1);
if (removedMaterial && humanAction.triggers[0].triggeredAsset?.triggeredAction?.actionUuid) {
triggerPointActions(humanAction, removedMaterial.materialId);
}
}
}
const handleAnimationComplete = (action: string) => {
if (action === 'starting') {
setAnimationPhase('first-hook-adjust');
} else if (action === 'picking') {
setCurrentPhase(crane.modelUuid, 'picking');
} else if (action === 'dropping') {
setCurrentPhase(crane.modelUuid, 'dropping');
}
}
return (
<>
<PillarJibAnimator
key={crane.modelUuid}
crane={crane}
points={points}
setPoints={setPoints}
animationPhase={animationPhase}
setAnimationPhase={setAnimationPhase}
onAnimationComplete={handleAnimationComplete}
/>
<MaterialAnimator crane={crane} />
<PillarJibHelper
crane={crane}
points={points}
isHelperNeeded={false}
/>
</>
)
}
export default PillarJibInstance;

View File

@@ -40,7 +40,7 @@ function PointsCalculator(
}
return {
points: [worldTopMiddle]
points: [new THREE.Vector3(worldTopMiddle.x, worldTopMiddle.y + 0.1, worldTopMiddle.z)]
};
}

View File

@@ -18,6 +18,7 @@ function PointInstances() {
machine: "purple",
storageUnit: "red",
human: "white",
crane: "yellow",
};
return (

View File

@@ -173,6 +173,22 @@ function TriggerConnector() {
});
});
}
// Handle Human point
else if (event.type === "crane" && 'point' in event) {
const point = event.point;
point.actions?.forEach(action => {
action.triggers?.forEach(trigger => {
if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) {
newConnections.push({
id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`,
startPointUuid: point.uuid,
endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
trigger
});
}
});
});
}
});
setConnections(newConnections);
@@ -182,31 +198,23 @@ function TriggerConnector() {
const canvasElement = gl.domElement;
let drag = false;
let isRightMouseDown = false;
let isLeftMouseDown = false;
const onMouseDown = (evt: MouseEvent) => {
if (selectedAsset) {
clearSelectedAsset();
}
if (evt.button === 2) {
isRightMouseDown = true;
if (evt.button === 0) {
isLeftMouseDown = true;
drag = false;
}
};
const onMouseUp = (evt: MouseEvent) => {
if (evt.button === 2) {
isRightMouseDown = false;
if (evt.button === 0) {
isLeftMouseDown = false;
}
}
const onMouseMove = () => {
if (isRightMouseDown) {
drag = true;
}
};
const handleRightClick = (evt: MouseEvent) => {
if (drag) return;
evt.preventDefault();
@@ -368,13 +376,16 @@ function TriggerConnector() {
} else if (firstSelectedPoint) {
setFirstSelectedPoint(null);
}
}
const onMouseMove = () => {
drag = true;
};
if (subModule === 'mechanics' && toolMode === 'cursor' && selectedAction.actionId && selectedAction.actionName) {
canvasElement.addEventListener("mousedown", onMouseDown);
canvasElement.addEventListener("mouseup", onMouseUp);
canvasElement.addEventListener("mousemove", onMouseMove);
canvasElement.addEventListener('contextmenu', handleRightClick);
} else {
setFirstSelectedPoint(null);
}
@@ -383,7 +394,6 @@ function TriggerConnector() {
canvasElement.removeEventListener("mousedown", onMouseDown);
canvasElement.removeEventListener("mouseup", onMouseUp);
canvasElement.removeEventListener("mousemove", onMouseMove);
canvasElement.removeEventListener('contextmenu', handleRightClick);
};
}, [gl, subModule, selectedProduct, firstSelectedPoint, toolMode, selectedAction]);

View File

@@ -25,7 +25,7 @@ export function useHumanEventManager() {
const addHumanToMonitor = (humanId: string, callback: () => void, actionUuid: string) => {
const human = getHumanById(humanId);
const action = getActionByUuid(selectedProduct.productUuid, actionUuid);
if (!human || !action || (action.actionType !== 'assembly' && action.actionType !== 'worker') || !humanEventManagerRef.current) return;
if (!human || !action || (action.actionType !== 'assembly' && action.actionType !== 'worker' && action.actionType !== 'operator') || !humanEventManagerRef.current) return;
let state = humanEventManagerRef.current.humanStates.find(h => h.humanId === humanId);
if (!state) {
@@ -36,7 +36,7 @@ export function useHumanEventManager() {
const existingAction = state.actionQueue.find(a => a.actionUuid === actionUuid);
if (existingAction) {
const currentCount = existingAction.count ?? 0;
if (existingAction.actionType === 'worker') {
if (existingAction.actionType === 'worker' || existingAction.actionType === 'operator') {
if (currentCount < existingAction.maxLoadCount) {
existingAction.callback = callback;
existingAction.isMonitored = true;
@@ -98,8 +98,8 @@ export function useHumanEventManager() {
let conditionMet = false;
if (currentAction.actionType === 'worker') {
if (action.actionType === 'worker' && human.currentLoad < currentAction.loadCapacity) {
if (currentAction.actionType === 'worker' || currentAction.actionType === 'operator') {
if ((action.actionType === 'worker' || action.actionType === 'operator') && human.currentLoad < currentAction.loadCapacity) {
conditionMet = true;
} else if (action.actionType === 'assembly') {
conditionMet = true;
@@ -107,7 +107,7 @@ export function useHumanEventManager() {
} else if (currentAction.actionType === 'assembly') {
if (action.actionType === 'assembly') {
conditionMet = true;
} else if (action.actionType === 'worker' && human.currentLoad < currentAction.loadCapacity) {
} else if ((action.actionType === 'worker' || action.actionType === 'operator') && human.currentLoad < currentAction.loadCapacity) {
conditionMet = true;
}
}
@@ -121,7 +121,7 @@ export function useHumanEventManager() {
action.callback();
action.count = (action.count ?? 0) + 1;
action.isMonitored = false;
if ((action.actionType === 'worker' && action.count >= action.maxLoadCount) ||
if (((action.actionType === 'worker' || action.actionType === 'operator') && action.count >= action.maxLoadCount) ||
(action.actionType === 'assembly' && action.count >= action.maxAssemblyCount)) {
action.isCompleted = true;
}

View File

@@ -53,7 +53,7 @@ const MaterialAnimator = ({ human }: { human: HumanStatus; }) => {
{hasLoad && action && (action as HumanAction).actionType === 'worker' && human.currentMaterials.length > 0 && (human.currentPhase !== 'init-pickup' && human.currentPhase !== 'init-assembly' && human.currentPhase !== 'drop-pickup') && (
<MaterialModel
matRef={meshRef}
materialId={human.currentMaterials[0].materialId || ''}
materialId={`human-${human.currentMaterials[0].materialId}` || ''}
materialType={human.currentMaterials[0].materialType || 'Default material'}
visible={isAttached}
/>

View File

@@ -0,0 +1,182 @@
import { useEffect, useRef, useState } from 'react';
import { useFrame, useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../products/productContext';
interface WorkerAnimatorProps {
path: [number, number, number][];
handleCallBack: () => void;
reset: () => void;
human: HumanStatus;
}
function OperatorAnimator({ path, handleCallBack, human, reset }: Readonly<WorkerAnimatorProps>) {
const { humanStore, assetStore, productStore } = useSceneContext();
const { getActionByUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { getHumanById } = humanStore();
const { setCurrentAnimation } = assetStore();
const { isPaused } = usePauseButtonStore();
const { isPlaying } = usePlayButtonStore();
const { speed } = useAnimationPlaySpeed();
const { isReset, setReset } = useResetButtonStore();
const progressRef = useRef<number>(0);
const movingForward = useRef<boolean>(true);
const completedRef = useRef<boolean>(false);
const action = getActionByUuid(selectedProduct.productUuid, human?.currentAction?.actionUuid || '');
const [objectRotation, setObjectRotation] = useState<[number, number, number] | null>((action as HumanAction)?.pickUpPoint?.rotation || [0, 0, 0]);
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const { scene } = useThree();
useEffect(() => {
if (!human.currentAction?.actionUuid) return;
const action = getActionByUuid(selectedProduct.productUuid, human?.currentAction?.actionUuid || '');
if (human.currentPhase === 'init-loadPoint' && path.length > 0) {
setCurrentPath(path);
setObjectRotation((action as HumanAction).pickUpPoint?.rotation ?? null);
} else if (human.currentPhase === 'loadPoint-unloadPoint' && path.length > 0) {
setObjectRotation((action as HumanAction)?.dropPoint?.rotation ?? null);
setCurrentPath(path);
} else if (human.currentPhase === 'unloadPoint-loadPoint' && path.length > 0) {
setObjectRotation((action as HumanAction)?.pickUpPoint?.rotation ?? null);
setCurrentPath(path);
}
}, [human.currentPhase, path, objectRotation, selectedProduct, human.currentAction?.actionUuid]);
useEffect(() => {
completedRef.current = false;
}, [currentPath]);
useEffect(() => {
if (isReset || !isPlaying) {
reset();
setCurrentPath([]);
completedRef.current = false;
movingForward.current = true;
progressRef.current = 0;
setReset(false);
setRestingRotation(true);
const object = scene.getObjectByProperty('uuid', human.modelUuid);
const humanData = getHumanById(human.modelUuid);
if (object && humanData) {
object.position.set(humanData.position[0], humanData.position[1], humanData.position[2]);
object.rotation.set(humanData.rotation[0], humanData.rotation[1], humanData.rotation[2]);
}
}
}, [isReset, isPlaying]);
const lastTimeRef = useRef(performance.now());
useFrame(() => {
const now = performance.now();
const delta = (now - lastTimeRef.current) / 1000;
lastTimeRef.current = now;
const object = scene.getObjectByProperty('uuid', human.modelUuid);
if (!object || currentPath.length < 2) return;
if (isPaused || !isPlaying) return;
let totalDistance = 0;
const distances = [];
let accumulatedDistance = 0;
let index = 0;
const rotationSpeed = 1.5;
for (let i = 0; i < currentPath.length - 1; i++) {
const start = new THREE.Vector3(...currentPath[i]);
const end = new THREE.Vector3(...currentPath[i + 1]);
const segmentDistance = start.distanceTo(end);
distances.push(segmentDistance);
totalDistance += segmentDistance;
}
while (index < distances.length && progressRef.current > accumulatedDistance + distances[index]) {
accumulatedDistance += distances[index];
index++;
}
if (index < distances.length) {
const start = new THREE.Vector3(...currentPath[index]);
const end = new THREE.Vector3(...currentPath[index + 1]);
const segmentDistance = distances[index];
const targetQuaternion = new THREE.Quaternion().setFromRotationMatrix(
new THREE.Matrix4().lookAt(start, end, new THREE.Vector3(0, 1, 0))
);
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
targetQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
} else {
const step = rotationSpeed * delta * speed * human.speed;
object.quaternion.rotateTowards(targetQuaternion, step);
}
const isAligned = angle < 0.01;
if (isAligned) {
progressRef.current += delta * (speed * human.speed);
const t = (progressRef.current - accumulatedDistance) / segmentDistance;
const position = start.clone().lerp(end, t);
object.position.copy(position);
setCurrentAnimation(human.modelUuid, 'walking', true, true, true);
} else {
setCurrentAnimation(human.modelUuid, 'idle', true, true, true);
}
}
if (progressRef.current >= totalDistance) {
if (restRotation && objectRotation) {
const targetEuler = new THREE.Euler(0, objectRotation[1], 0);
const baseQuaternion = new THREE.Quaternion().setFromEuler(targetEuler);
const y180 = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
const targetQuaternion = baseQuaternion.multiply(y180);
const angle = object.quaternion.angleTo(targetQuaternion);
if (angle < 0.01) {
object.quaternion.copy(targetQuaternion);
setRestingRotation(false);
} else {
const step = rotationSpeed * delta * speed * human.speed;
object.quaternion.rotateTowards(targetQuaternion, step);
}
setCurrentAnimation(human.modelUuid, 'idle', true, true, true);
return;
}
}
if (progressRef.current >= totalDistance) {
setRestingRotation(true);
progressRef.current = 0;
movingForward.current = !movingForward.current;
setCurrentPath([]);
handleCallBack();
}
});
return (
<>
{currentPath.length > 0 && (
<group visible={false}>
<Line points={currentPath} color="blue" lineWidth={3} />
{currentPath.map((point, index) => (
<mesh key={index} position={point}>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="red" />
</mesh>
))}
</group>
)}
</>
);
}
export default OperatorAnimator;

View File

@@ -0,0 +1,168 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { useThree } from '@react-three/fiber';
import { NavMeshQuery } from '@recast-navigation/core';
import { useNavMesh } from '../../../../../../store/builder/store';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore } from '../../../../../../store/usePlayButtonStore';
import { useTriggerHandler } from '../../../../triggers/triggerHandler/useTriggerHandler';
import { useSceneContext } from '../../../../../scene/sceneContext';
import { useProductContext } from '../../../../products/productContext';
import OperatorAnimator from '../../animator/operatorAnimator';
function OperatorInstance({ human }: { human: HumanStatus }) {
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
const { scene } = useThree();
const { assetStore, materialStore, armBotStore, conveyorStore, machineStore, vehicleStore, humanStore, storageUnitStore, craneStore, productStore } = useSceneContext();
const { removeMaterial, setEndTime, setIsVisible } = materialStore();
const { getStorageUnitById } = storageUnitStore();
const { getArmBotById } = armBotStore();
const { getConveyorById } = conveyorStore();
const { getVehicleById } = vehicleStore();
const { getMachineById } = machineStore();
const { getCraneById } = craneStore();
const { triggerPointActions } = useTriggerHandler();
const { setCurrentAnimation, resetAnimation, getAssetById } = assetStore();
const { getActionByUuid, getEventByModelUuid, getTriggerByUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { setHumanActive, setHumanState, clearCurrentMaterials, setHumanLoad, setHumanScheduled, decrementHumanLoad, removeLastMaterial, incrementIdleTime, incrementActiveTime, resetTime, setCurrentPhase } = humanStore();
const [path, setPath] = useState<[number, number, number][]>([]);
const pauseTimeRef = useRef<number | null>(null);
const idleTimeRef = useRef<number>(0);
const activeTimeRef = useRef<number>(0);
const isPausedRef = useRef<boolean>(false);
const isSpeedRef = useRef<number>(0);
const { speed } = useAnimationPlaySpeed();
const { isPaused } = usePauseButtonStore();
const previousTimeRef = useRef<number | null>(null);
const animationFrameIdRef = useRef<number | null>(null);
const humanAsset = getAssetById(human.modelUuid);
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
useEffect(() => {
isSpeedRef.current = speed;
}, [speed]);
const computePath = useCallback((start: [number, number, number], end: [number, number, number]) => {
try {
const navMeshQuery = new NavMeshQuery(navMesh);
let startPoint = new THREE.Vector3(start[0], start[1], start[2]);
let endPoint = new THREE.Vector3(end[0], end[1], end[2]);
const { path: segmentPath } = navMeshQuery.computePath(startPoint, endPoint);
if (
segmentPath.length > 0 &&
Math.round(segmentPath[segmentPath.length - 1].x) == Math.round(endPoint.x) &&
Math.round(segmentPath[segmentPath.length - 1].z) == Math.round(endPoint.z)
) {
return segmentPath?.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || [];
} else {
console.log("There is no path here...Choose valid path")
const { path: segmentPaths } = navMeshQuery.computePath(startPoint, startPoint);
return segmentPaths.map(({ x, y, z }) => [x, 0, z] as [number, number, number]) || [];
}
} catch {
console.error("Failed to compute path");
return [];
}
}, [navMesh]);
function humanStatus(modelId: string, status: string) {
// console.log(`${modelId} , ${status}`);
}
function reset() {
setCurrentPhase(human.modelUuid, 'init');
setHumanActive(human.modelUuid, false);
setHumanState(human.modelUuid, 'idle');
setHumanScheduled(human.modelUuid, false);
setHumanLoad(human.modelUuid, 0);
resetAnimation(human.modelUuid);
setPath([]);
isPausedRef.current = false;
pauseTimeRef.current = 0;
resetTime(human.modelUuid)
activeTimeRef.current = 0
idleTimeRef.current = 0
previousTimeRef.current = null
if (animationFrameIdRef.current !== null) {
cancelAnimationFrame(animationFrameIdRef.current)
animationFrameIdRef.current = null
}
const object = scene.getObjectByProperty('uuid', human.modelUuid);
if (object && human) {
object.position.set(human.position[0], human.position[1], human.position[2]);
object.rotation.set(human.rotation[0], human.rotation[1], human.rotation[2]);
}
}
useEffect(() => {
if (isPlaying) {
const action = getActionByUuid(selectedProduct.productUuid, human?.currentAction?.actionUuid || '');
if (!action || action.actionType !== 'operator' || !action.pickUpPoint || !action.dropPoint) return;
if (!human.isActive && human.state === 'idle' && human.currentPhase === 'init') {
const humanMesh = scene.getObjectByProperty('uuid', human.modelUuid);
if (!humanMesh) return;
const toPickupPath = computePath(humanMesh.position.toArray(), action?.pickUpPoint?.position || [0, 0, 0]);
setPath(toPickupPath);
setCurrentPhase(human.modelUuid, 'init-loadPoint');
setHumanState(human.modelUuid, 'running');
setHumanActive(human.modelUuid, true);
setCurrentAnimation(human.modelUuid, 'walking', true, true, true);
humanStatus(human.modelUuid, 'Started from init, heading to loadPoint');
} else if (human.isActive && human.currentPhase === 'loadPoint-unloadPoint') {
if (action.pickUpPoint && action.dropPoint && humanAsset?.animationState?.current === 'idle') {
const toDrop = computePath(action.pickUpPoint.position || [0, 0, 0], action.dropPoint.position || [0, 0, 0]);
setPath(toDrop);
setCurrentAnimation(human.modelUuid, 'walking', true, true, true);
humanStatus(human.modelUuid, 'Started from loadPoint, heading to unloadPoint');
}
} else if (human.state === 'idle' && human.currentPhase === 'unhooking') {
setHumanState(human.modelUuid, 'running');
setHumanActive(human.modelUuid, true);
setCurrentAnimation(human.modelUuid, 'working_standing', true, false, false);
}
} else {
reset()
}
}, [human, human.currentAction, human.currentPhase, path, isPlaying, humanAsset?.animationState?.isCompleted]);
function handleCallBack() {
if (human.currentPhase === 'init-loadPoint') {
setCurrentPhase(human.modelUuid, 'waiting');
setHumanState(human.modelUuid, 'idle');
setHumanActive(human.modelUuid, false);
setCurrentAnimation(human.modelUuid, 'idle', true, true, true);
humanStatus(human.modelUuid, 'Reached loadPoint point, waiting for material');
setPath([]);
} else if (human.currentPhase === 'loadPoint-unloadPoint') {
setCurrentPhase(human.modelUuid, 'unhooking');
setHumanState(human.modelUuid, 'idle');
setHumanActive(human.modelUuid, false);
setCurrentAnimation(human.modelUuid, 'idle', true, true, true);
humanStatus(human.modelUuid, 'Reached loadPoint point, waiting for material');
setPath([]);
}
}
return (
<>
<OperatorAnimator
path={path}
handleCallBack={handleCallBack}
human={human}
reset={reset}
/>
</>
)
}
export default OperatorInstance;

View File

@@ -6,6 +6,7 @@ import { useProductContext } from '../../../products/productContext';
import MaterialAnimator from '../animator/materialAnimator';
import AssemblerInstance from './actions/assemberInstance';
import WorkerInstance from './actions/workerInstance';
import OperatorInstance from './actions/operatorInstance';
function HumanInstance({ human }: { human: HumanStatus }) {
const { isPlaying } = usePlayButtonStore();
@@ -86,6 +87,9 @@ function HumanInstance({ human }: { human: HumanStatus }) {
{action && action.actionType === 'assembly' &&
<AssemblerInstance human={human} />
}
{action && action.actionType === 'operator' &&
<OperatorInstance human={human} />
}
<MaterialAnimator human={human} />
</>

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react'
import { useGLTF } from '@react-three/drei';
import * as THREE from 'three'
import { Tube, useGLTF } from '@react-three/drei';
import { useFrame, useThree } from '@react-three/fiber';
import { useIsDragging, useIsRotating, useSelectedAction, useSelectedEventSphere } from '../../../../../store/simulation/useSimulationStore';
import { useProductContext } from '../../../products/productContext';
@@ -27,7 +28,7 @@ function HumanUi() {
const { humanStore, productStore } = useSceneContext();
const { selectedProduct } = selectedProductStore();
const { humans, getHumanById } = humanStore();
const { updateEvent, updateAction, getActionByUuid } = productStore();
const { updateEvent, getActionByUuid } = productStore();
const [startPosition, setStartPosition] = useState<[number, number, number]>([0, 1, 0]);
const [endPosition, setEndPosition] = useState<[number, number, number]>([0, 1, 0]);
const [assemblyPosition, setAssemblyPosition] = useState<[number, number, number]>([0, 1, 0]);
@@ -313,64 +314,49 @@ function HumanUi() {
rotation={[0, Math.PI, 0]}
>
{isAssembly ? (
<primitive
ref={assemblyMarker}
<MarkerPrimitive
name="assemblyMarker"
refProp={assemblyMarker}
object={assemblyScene}
position={assemblyPosition}
rotation={assemblyRotation}
onPointerDown={(e: any) => {
if (e.object.parent.name === "handle") {
handlePointerDown(e, "assembly", "assembly");
} else {
handlePointerDown(e, "assembly", "assembly");
}
}}
onPointerMissed={() => {
setIsDragging(null);
setIsRotating(null);
if (controls) (controls as any).enabled = true;
}}
outerGroupRef={outerGroup}
type="assembly"
subtype="assembly"
color="#0f87f7"
setIsDragging={setIsDragging}
setIsRotating={setIsRotating}
handlePointerDown={handlePointerDown}
/>
) : (
<>
<primitive
<MarkerPrimitive
name="startMarker"
refProp={startMarker}
object={startScene}
ref={startMarker}
position={startPosition}
rotation={startRotation}
onPointerDown={(e: any) => {
if (e.object.parent.name === "handle") {
handlePointerDown(e, "start", "start");
} else {
handlePointerDown(e, "start", "start");
}
}}
onPointerMissed={() => {
setIsDragging(null);
setIsRotating(null);
if (controls) (controls as any).enabled = true;
}}
outerGroupRef={outerGroup}
type="start"
subtype="start"
color="lightgreen"
setIsDragging={setIsDragging}
setIsRotating={setIsRotating}
handlePointerDown={handlePointerDown}
/>
<primitive
<MarkerPrimitive
name="endMarker"
refProp={endMarker}
object={endScene}
ref={endMarker}
position={endPosition}
rotation={endRotation}
onPointerDown={(e: any) => {
if (e.object.parent.name === "handle") {
handlePointerDown(e, "end", "end");
} else {
handlePointerDown(e, "end", "end");
}
}}
onPointerMissed={() => {
setIsDragging(null);
setIsRotating(null);
if (controls) (controls as any).enabled = true;
}}
outerGroupRef={outerGroup}
type="end"
subtype="end"
color="darkorange"
setIsDragging={setIsDragging}
setIsRotating={setIsRotating}
handlePointerDown={handlePointerDown}
/>
</>
)}
@@ -380,4 +366,101 @@ function HumanUi() {
);
}
export default HumanUi;
export default HumanUi;
const MarkerPrimitive = ({
name,
refProp,
object,
position,
rotation,
outerGroupRef,
type,
subtype,
color,
setIsDragging,
setIsRotating,
handlePointerDown,
}: {
name: string;
refProp: any;
object: THREE.Object3D;
position: [number, number, number];
rotation: [number, number, number];
outerGroupRef: React.RefObject<THREE.Group>;
type: string;
subtype: string;
color: string;
setIsDragging: (val: any) => void;
setIsRotating: (val: any) => void;
handlePointerDown: any;
}) => {
const { controls, scene, camera } = useThree();
const [hitPoint, setHitPoint] = useState<THREE.Vector3 | null>(null);
const [curve, setCurve] = useState<THREE.CatmullRomCurve3 | null>(null);
useFrame(() => {
if (!refProp.current || !outerGroupRef.current || !scene) return;
const worldPos = new THREE.Vector3();
refProp.current.getWorldPosition(worldPos);
const localMarkerPos = outerGroupRef.current.worldToLocal(worldPos.clone());
const rayOrigin = worldPos.clone();
const direction = new THREE.Vector3(0, -1, 0);
const raycaster = new THREE.Raycaster(rayOrigin, direction, 0.1, 1000);
raycaster.camera = camera;
const intersects = raycaster.intersectObjects(scene.children, true);
const hit = intersects.find(i => i.object.name !== name);
if (hit) {
const localHit = outerGroupRef.current.worldToLocal(hit.point.clone());
setHitPoint(localHit);
const newCurve = new THREE.CatmullRomCurve3([
localMarkerPos.clone(),
localHit.clone(),
]);
setCurve(newCurve);
} else {
setHitPoint(null);
setCurve(null);
}
});
return (
<>
<primitive
name={name}
ref={refProp}
object={object}
position={position}
rotation={rotation}
onPointerDown={(e: any) => {
handlePointerDown(e, type, subtype);
}}
onPointerMissed={() => {
setIsDragging(null);
setIsRotating(null);
if (controls) (controls as any).enabled = true;
}}
/>
{hitPoint && (
<>
<mesh name={name} position={hitPoint} rotation={[-Math.PI / 2, 0, 0]}>
<torusGeometry args={[0.15, 0.02, 3, 32]} />
<meshBasicMaterial color={color} depthWrite={false} />
</mesh>
{curve && (
<Tube args={[curve, 20, 0.01, 8, true]} >
<meshBasicMaterial color={color} depthWrite={false} />
</Tube>
)}
</>
)}
</>
);
};

View File

@@ -61,30 +61,12 @@ function MaterialInstance({ material }: { readonly material: MaterialSchema }) {
function getCurrentSpeed(productUuid: string, modelUuid: string) {
const event = getEventByModelUuid(productUuid, modelUuid)
if (event) {
if (event.type === 'transfer') {
return event.speed;
}
if (event.type === 'vehicle') {
return event.speed;
}
if (event.type === 'machine') {
if (event.type === 'transfer' || event.type === 'machine' || event.type === 'storageUnit') {
return 1;
}
if (event.type === 'roboticArm') {
if (event.type === 'vehicle' || event.type === 'roboticArm' || event.type === 'human') {
return event.speed;
}
if (event.type === 'storageUnit') {
return 1;
}
if (event.type === 'human') {
return event.speed;
}
} else {
return 1;
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react'
import { useEffect } from 'react'
import MaterialInstance from './instance/materialInstance'
import { useSceneContext } from '../../../scene/sceneContext';

View File

@@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom';
import { useVersionContext } from '../../builder/version/versionContext';
function Products() {
const { armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, layout, productStore } = useSceneContext();
const { armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, layout, productStore } = useSceneContext();
const { products, getProductById, addProduct, setProducts } = productStore();
const { selectedProductStore } = useProductContext();
const { setMainProduct } = useMainProduct();
@@ -20,7 +20,8 @@ function Products() {
const { addMachine, clearMachines } = machineStore();
const { addConveyor, clearConveyors } = conveyorStore();
const { setCurrentMaterials, clearStorageUnits, updateCurrentLoad, addStorageUnit } = storageUnitStore();
const { addHuman, addCurrentAction, clearHumans } = humanStore();
const { addHuman, addCurrentAction: addCurrentActionHuman, clearHumans } = humanStore();
const { addCrane, addCurrentAction: addCurrentActionCrane, clearCranes } = craneStore();
const { isReset } = useResetButtonStore();
const { isPlaying } = usePlayButtonStore();
const { mainProduct } = useMainProduct();
@@ -164,7 +165,25 @@ function Products() {
addHuman(selectedProduct.productUuid, events);
if (events.point.actions.length > 0) {
addCurrentAction(events.modelUuid, events.point.actions[0].actionUuid);
addCurrentActionHuman(events.modelUuid, events.point.actions[0].actionUuid);
}
}
});
}
}
}, [selectedProduct, products, isReset, isPlaying]);
useEffect(() => {
if (selectedProduct.productUuid) {
const product = getProductById(selectedProduct.productUuid);
if (product) {
clearCranes();
product.eventDatas.forEach(events => {
if (events.type === 'crane') {
addCrane(selectedProduct.productUuid, events);
if (events.point.actions.length > 0) {
addCurrentActionCrane(events.modelUuid, events.point.actions[0].actionUuid, null, null);
}
}
});

View File

@@ -1,15 +1,27 @@
import { useEffect, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
import { useFrame, useThree } from '@react-three/fiber';
import * as THREE from 'three';
import { Line, Text } from '@react-three/drei';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useProductContext } from '../../../products/productContext';
type PointWithDegree = {
position: [number, number, number];
degree: number;
};
function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone, armBot, path }: any) {
interface RoboticArmAnimatorProps {
HandleCallback: () => void;
restPosition: THREE.Vector3;
ikSolver: any;
targetBone: string;
armBot: ArmBotStatus;
path: [number, number, number][];
currentPhase: string;
}
function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone, armBot, path, currentPhase }: RoboticArmAnimatorProps) {
const progressRef = useRef(0);
const curveRef = useRef<THREE.Vector3[] | null>(null);
const totalDistanceRef = useRef(0);
@@ -19,6 +31,14 @@ function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone
const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]);
const [circlePointsWithDegrees, setCirclePointsWithDegrees] = useState<PointWithDegree[]>([]);
const [customCurvePoints, setCustomCurvePoints] = useState<THREE.Vector3[] | null>(null);
const { armBotStore, productStore, materialStore } = useSceneContext();
const { getArmBotById } = armBotStore();
const { getMaterialById, getMaterialPosition } = materialStore();
const { getEventByModelUuid, getActionByUuid, getPointByUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { scene } = useThree();
let curveHeight = 1.75
const CIRCLE_RADIUS = 1.6
@@ -143,10 +163,106 @@ function RoboticArmAnimator({ HandleCallback, restPosition, ikSolver, targetBone
// Handle nearest points and final path (including arc points)
useEffect(() => {
if (circlePoints.length > 0 && currentPath.length > 0) {
if (circlePoints.length > 0 && currentPath.length > 0 && ikSolver.mesh) {
const start = currentPath[0];
const end = currentPath[currentPath.length - 1];
let start = currentPath[0];
let end = currentPath[currentPath.length - 1];
const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone);
const armbotStatus = getArmBotById(armBot.modelUuid);
const currentMaterial = armbotStatus?.currentAction?.materialId;
const currentAction = getActionByUuid(selectedProduct.productUuid, armbotStatus?.currentAction?.actionUuid || '');
if (armbotStatus && currentMaterial && currentAction && (currentPhase === 'rest-to-start' || currentPhase === 'start-to-end' || currentPhase === 'end-to-rest')) {
const materialData = getMaterialById(currentMaterial);
if (materialData) {
const prevModel = getEventByModelUuid(selectedProduct.productUuid, materialData.current.modelUuid);
const nextModel = getEventByModelUuid(selectedProduct.productUuid, currentAction?.triggers[0]?.triggeredAsset?.triggeredModel?.modelUuid || '');
const nextPoint = getPointByUuid(selectedProduct.productUuid, currentAction?.triggers[0]?.triggeredAsset?.triggeredModel?.modelUuid || '', currentAction?.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid || '');
if (prevModel && prevModel.type === 'transfer') {
const material = scene.getObjectByProperty("uuid", currentMaterial);
const armbotModel = scene.getObjectByProperty("uuid", armBot.modelUuid);
if (material && armbotModel) {
const materialWorldPos = new THREE.Vector3();
material.getWorldPosition(materialWorldPos);
const armbotWorldPos = new THREE.Vector3();
armbotModel.getWorldPosition(armbotWorldPos);
const materialLocalPos = materialWorldPos.clone();
armbotModel.worldToLocal(materialLocalPos);
if (currentPhase === 'rest-to-start') {
end = [materialLocalPos.x, materialLocalPos.y + 0.35, materialLocalPos.z];
} else if (currentPhase === 'start-to-end') {
start = [materialLocalPos.x, materialLocalPos.y + 0.35, materialLocalPos.z];
}
}
} else if (prevModel && prevModel.type === 'storageUnit') {
const position = getMaterialPosition(prevModel.modelUuid, currentMaterial);
const armbotModel = scene.getObjectByProperty("uuid", armBot.modelUuid);
if (armbotModel) {
const armbotWorldPos = new THREE.Vector3();
let materialWorldPos = new THREE.Vector3();
if (position) {
materialWorldPos.copy(position);
} else {
materialWorldPos.copy(bone.getWorldPosition(armbotWorldPos));
}
const materialLocalPos = materialWorldPos.clone();
armbotModel.worldToLocal(materialLocalPos);
if (currentPhase === 'rest-to-start') {
end = [materialLocalPos.x, materialLocalPos.y + 0.35, materialLocalPos.z];
} else if (currentPhase === 'start-to-end') {
start = [materialLocalPos.x, materialLocalPos.y + 0.35, materialLocalPos.z];
} else if (currentPhase === 'end-to-rest') {
start = [materialLocalPos.x, materialLocalPos.y + 0.35, materialLocalPos.z];
}
}
}
if (nextModel && nextPoint && nextModel.type === 'transfer') {
const conveyorModel = scene.getObjectByProperty("uuid", nextModel.modelUuid);
const armbotModel = scene.getObjectByProperty("uuid", armBot.modelUuid);
if (conveyorModel && armbotModel) {
const localPoint = new THREE.Vector3(
nextPoint.position[0],
nextPoint.position[1],
nextPoint.position[2]
);
const worldPoint = conveyorModel.localToWorld(localPoint);
armbotModel.worldToLocal(worldPoint);
if (currentPhase === 'start-to-end') {
end = [worldPoint.x, worldPoint.y + 0.35, worldPoint.z];
}
}
}
}
}
if (currentPhase === 'end-to-rest') {
const armbotModel = scene.getObjectByProperty("uuid", armBot.modelUuid);
const armbotWorldPos = new THREE.Vector3();
if (armbotModel) {
let materialWorldPos = new THREE.Vector3();
materialWorldPos.copy(bone.getWorldPosition(armbotWorldPos));
const materialLocalPos = materialWorldPos.clone();
armbotModel.worldToLocal(materialLocalPos);
start = [materialLocalPos.x, materialLocalPos.y + 0.35, materialLocalPos.z];
}
}
const raisedStart = [start[0], start[1] + 0.5, start[2]] as [number, number, number];
const raisedEnd = [end[0], end[1] + 0.5, end[2]] as [number, number, number];

View File

@@ -334,7 +334,6 @@ function RoboticArmInstance({ armBot }: { readonly armBot: ArmBotStatus }) {
}, [currentPhase, armBot, isPlaying, isReset, ikSolver])
function createCurveBetweenTwoPoints(p1: any, p2: any) {
const mid = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5);
const points = [p1, mid, p2];
@@ -342,7 +341,6 @@ function RoboticArmInstance({ armBot }: { readonly armBot: ArmBotStatus }) {
}
const HandleCallback = () => {
if (armBot.isActive && armBot.state == "running" && currentPhase == "init-to-rest") {
logStatus(armBot.modelUuid, "Callback triggered: rest");
setArmBotActive(armBot.modelUuid, false)
@@ -370,7 +368,6 @@ function RoboticArmInstance({ armBot }: { readonly armBot: ArmBotStatus }) {
setArmBotState(armBot.modelUuid, "idle")
setCurrentPhase("rest");
setPath([])
}
}
@@ -389,7 +386,6 @@ function RoboticArmInstance({ armBot }: { readonly armBot: ArmBotStatus }) {
ikSolver={ikSolver}
targetBone={targetBone}
armBot={armBot}
logStatus={logStatus}
path={path}
currentPhase={currentPhase}
/>

View File

@@ -22,7 +22,7 @@ function IKInstance({ setIkSolver, armBot }: IKInstanceProps) {
const trySetup = () => {
const targetMesh = scene?.getObjectByProperty("uuid", armBot.modelUuid);
if (!targetMesh || !targetMesh.userData.iks || targetMesh.userData.iks.length < 1) {
if (!targetMesh || !targetMesh.userData.fieldData || targetMesh.userData.fieldData.length < 1) {
retryId = setTimeout(trySetup, 100);
return;
}
@@ -34,8 +34,8 @@ function IKInstance({ setIkSolver, armBot }: IKInstanceProps) {
});
if (!OOI.Target_Bone || !OOI.Skinned_Mesh) return;
const rawIks: IK[] = targetMesh.userData.iks;
const iks = rawIks.map((ik) => ({
const rawIks: IK[] = targetMesh.userData.fieldData;
const fieldData = rawIks.map((ik) => ({
target: ik.target,
effector: ik.effector,
links: ik.links.map((link) => ({
@@ -51,10 +51,10 @@ function IKInstance({ setIkSolver, armBot }: IKInstanceProps) {
minheight: ik.minheight,
}));
const solver = new CCDIKSolver(OOI.Skinned_Mesh, iks);
const solver = new CCDIKSolver(OOI.Skinned_Mesh, fieldData);
setIkSolver(solver);
// const helper = new CCDIKHelper(OOI.Skinned_Mesh, iks, 0.05)
// const helper = new CCDIKHelper(OOI.Skinned_Mesh, fieldData, 0.05)
// scene.add(helper);
};

View File

@@ -7,6 +7,7 @@ import Materials from './materials/materials';
import Machine from './machine/machine';
import StorageUnit from './storageUnit/storageUnit';
import Human from './human/human';
import Crane from './crane/crane';
import Simulator from './simulator/simulator';
import Products from './products/products';
import Trigger from './triggers/trigger';
@@ -55,6 +56,8 @@ function Simulation() {
<Human />
<Crane />
<Simulator />
<SimulationAnalysis />

View File

@@ -125,7 +125,8 @@ function determineExecutionMachineSequences(products: productsSchema): EventsSch
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm' ||
event.type === 'human'
event.type === 'human' ||
event.type === 'crane'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);

View File

@@ -17,7 +17,8 @@ export async function determineExecutionMachineSequences(products: productsSchem
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm' ||
event.type === 'human'
event.type === 'human' ||
event.type === 'crane'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);

View File

@@ -20,7 +20,8 @@ export function determineExecutionOrder(products: productsSchema): PointsScheme[
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm' ||
event.type === 'human'
event.type === 'human' ||
event.type === 'crane'
) {
pointMap.set(event.point.uuid, event.point);
allPoints.push(event.point);

View File

@@ -17,7 +17,8 @@ export async function determineExecutionSequences(products: productsSchema): Pro
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm' ||
event.type === 'human'
event.type === 'human' ||
event.type === 'crane'
) {
pointMap.set(event.point.uuid, event.point);
allPoints.push(event.point);

View File

@@ -92,7 +92,8 @@ function determineExecutionMachineSequences(products: productsSchema): EventsSch
event.type === 'machine' ||
event.type === 'storageUnit' ||
event.type === 'roboticArm' ||
event.type === 'human'
event.type === 'human' ||
event.type === 'crane'
) {
pointToEventMap.set(event.point.uuid, event);
allPoints.push(event.point);

View File

@@ -55,6 +55,7 @@ const ArmBotUI = () => {
})
}
// Fetch and setup selected ArmBot data
useEffect(() => {
if (selectedEventSphere) {
const selectedArmBot = getEventByModelUuid(selectedProduct.productUuid, selectedEventSphere.userData.modelUuid);
@@ -85,11 +86,13 @@ const ArmBotUI = () => {
const modelData = getEventByModelUuid(selectedProduct.productUuid, modelUuid);
if (modelData?.type === "roboticArm") {
const baseY = modelData.point.position?.[1] || 0;
const baseX = modelData.point.position?.[0] || 0;
const baseY = modelData.point.position?.[1] || 0;;
const baseZ = modelData.point.position?.[2] || 0;
return {
pick: [0, baseY, 0 + 0.5],
drop: [0, baseY, 0 - 0.5],
default: [0, baseY, 0],
pick: [baseX, baseY, baseZ + 0.5],
drop: [baseX, baseY, baseZ - 0.5],
default: [baseX, baseY, baseZ],
};
}
@@ -170,13 +173,16 @@ const ArmBotUI = () => {
const targetMesh = scene?.getObjectByProperty("uuid", selectedArmBotData?.modelUuid || '');
const fieldData = targetMesh?.userData?.fieldData;
const firstIK = Array.isArray(fieldData) && fieldData.length > 0 ? fieldData[0] : {};
const { handlePointerDown } = useDraggableGLTF(
updatePointToState,
{
minDistance: targetMesh?.userData?.iks[0]?.minDistance || 1.2,
maxDistance: targetMesh?.userData?.iks[0]?.maxDistance || 2,
maxheight: targetMesh?.userData?.iks[0]?.maxheight || 0.6,
minheight: targetMesh?.userData?.iks[0]?.minheight || 1.9,
minDistance: firstIK.minDistance ?? 1.2,
maxDistance: firstIK.maxDistance ?? 2,
maxheight: firstIK.maxheight ?? 0.6,
minheight: firstIK.minheight ?? 1.9,
}
);
@@ -222,7 +228,7 @@ const ArmBotUI = () => {
</React.Fragment>
);
} else {
return null;
return null; // important! must return something
}
})}
</>

View File

@@ -183,7 +183,11 @@ export default function useDraggableGLTF(
targetPosition.z = centerZ + finalLocal.z;
// Clamp Y axis using variables
targetPosition.y = Math.min(Math.max(targetPosition.y, minHeight), maxHeight);
targetPosition.y = Math.min(
Math.max(targetPosition.y, Math.min(minHeight, maxHeight)),
Math.max(minHeight, maxHeight)
);
// Convert to local if parent exists
if (parent) {

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import * as Types from "../../../../types/world/worldTypes";
import { useGLTF } from "@react-three/drei";
import { Tube, useGLTF } from "@react-three/drei";
import * as THREE from "three";
import { useFrame, useThree } from "@react-three/fiber";
import { useSelectedEventSphere, useIsDragging, useIsRotating, } from "../../../../store/simulation/useSimulationStore";
import { upsertProductOrEventApi } from "../../../../services/simulation/products/UpsertProductOrEventApi";
@@ -16,8 +17,6 @@ import { useVersionContext } from "../../../builder/version/versionContext";
const VehicleUI = () => {
const { scene: startScene } = useGLTF(startPoint) as any;
const { scene: endScene } = useGLTF(startEnd) as any;
const startMarker = useRef<Group>(null);
const endMarker = useRef<Group>(null);
const prevMousePos = useRef({ x: 0, y: 0 });
const { selectedEventSphere } = useSelectedEventSphere();
const { selectedProductStore } = useProductContext();
@@ -162,7 +161,7 @@ const VehicleUI = () => {
let globalStartPosition = null;
let globalEndPosition = null;
if (outerGroup.current && startMarker.current && endMarker.current) {
if (outerGroup.current) {
const worldPosStart = new Vector3(...startPosition);
globalStartPosition = outerGroup.current.localToWorld(
worldPosStart.clone()
@@ -277,24 +276,19 @@ const VehicleUI = () => {
const currentPointerX = state.pointer.x;
const deltaX = currentPointerX - prevMousePos.current.x;
prevMousePos.current.x = currentPointerX;
const marker =
isRotating === "start" ? startMarker.current : endMarker.current;
if (marker) {
const rotationSpeed = 10;
marker.rotation.y += deltaX * rotationSpeed;
if (isRotating === "start") {
setStartRotation([
marker.rotation.x,
marker.rotation.y,
marker.rotation.z,
]);
} else {
setEndRotation([
marker.rotation.x,
marker.rotation.y,
marker.rotation.z,
]);
}
const rotationSpeed = 10;
if (isRotating === "start") {
setStartRotation([
startRotation[0],
startRotation[1] + deltaX * rotationSpeed,
startRotation[2],
]);
} else {
setEndRotation([
endRotation[0],
endRotation[1] + deltaX * rotationSpeed,
endRotation[2],
]);
}
});
@@ -342,41 +336,130 @@ const VehicleUI = () => {
</group>
{/* Start Marker */}
<primitive
<VehicleMarkerPrimitive
name="startMarker"
object={startScene}
ref={startMarker}
position={startPosition}
rotation={startRotation}
onPointerDown={(e: any) => {
e.stopPropagation();
handlePointerDown(e, "start", "start");
}}
outerGroupRef={outerGroup}
color="green"
handlePointerDown={handlePointerDown}
setIsDragging={setIsDragging}
setIsRotating={setIsRotating}
/>
{/* End Marker */}
<VehicleMarkerPrimitive
name="endMarker"
object={endScene}
position={endPosition}
rotation={endRotation}
outerGroupRef={outerGroup}
color="orange"
handlePointerDown={handlePointerDown}
setIsDragging={setIsDragging}
setIsRotating={setIsRotating}
/>
</group>
) : null;
};
export default VehicleUI;
export const VehicleMarkerPrimitive = ({
name,
object,
position,
rotation,
outerGroupRef,
color,
handlePointerDown,
setIsDragging,
setIsRotating,
}: {
name: string;
object: THREE.Object3D;
position: [number, number, number];
rotation: [number, number, number];
outerGroupRef: React.RefObject<THREE.Group>;
color: string;
handlePointerDown: (e: any, type: "start" | "end", rotation: "start" | "end") => void;
setIsDragging: (val: any) => void;
setIsRotating: (val: any) => void;
}) => {
const { scene, camera } = useThree();
const markerRef = useRef<THREE.Group>(null);
const [hitPoint, setHitPoint] = useState<THREE.Vector3 | null>(null);
const [curve, setCurve] = useState<THREE.CatmullRomCurve3 | null>(null);
useFrame(() => {
if (!markerRef.current || !outerGroupRef.current) return;
const worldPos = new THREE.Vector3();
markerRef.current.getWorldPosition(worldPos);
const localMarkerPos = outerGroupRef.current.worldToLocal(worldPos.clone());
const rayOrigin = worldPos.clone();
const direction = new THREE.Vector3(0, -1, 0);
const raycaster = new THREE.Raycaster(rayOrigin, direction, 0.1, 1000);
raycaster.camera = camera;
const intersects = raycaster.intersectObjects(scene.children, true);
const hit = intersects.find(i => i.object.name !== name);
if (hit) {
const localHit = outerGroupRef.current.worldToLocal(hit.point.clone());
setHitPoint(localHit);
const newCurve = new THREE.CatmullRomCurve3([
localMarkerPos.clone(),
localHit.clone(),
]);
setCurve(newCurve);
} else {
setHitPoint(null);
setCurve(null);
}
});
return (
<>
<primitive
name={name}
ref={markerRef}
object={object}
position={position}
rotation={rotation}
onPointerDown={(e: any) =>
handlePointerDown(
e,
name === "startMarker" ? "start" : "end",
name === "startMarker" ? "start" : "end"
)
}
onPointerMissed={() => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
}}
/>
{/* End Marker */}
<primitive
name="endMarker"
object={endScene}
ref={endMarker}
position={endPosition}
rotation={endRotation}
onPointerDown={(e: any) => {
e.stopPropagation();
handlePointerDown(e, "end", "end");
}}
onPointerMissed={() => {
controls.enabled = true;
setIsDragging(null);
setIsRotating(null);
}}
/>
</group>
) : null;
};
export default VehicleUI;
{hitPoint && (
<>
<mesh
name={`${name}-torus`}
position={hitPoint}
rotation={[-Math.PI / 2, 0, 0]}
>
<torusGeometry args={[0.15, 0.02, 3, 32]} />
<meshBasicMaterial color={color} depthWrite={false} />
</mesh>
{curve && (
<Tube args={[curve, 20, 0.01, 8, true]}>
<meshBasicMaterial color={color} depthWrite={false} />
</Tube>
)}
</>
)}
</>
);
};

View File

@@ -1,72 +1,84 @@
import { useRef, useMemo } from "react";
import { MaterialModel } from "../../../materials/instances/material/materialModel";
import { useEffect, useMemo } from "react";
import { Object3D, Box3, Vector3 } from "three";
import { useThree } from "@react-three/fiber";
import { useLoadingProgress } from "../../../../../store/builder/store";
import { MaterialModel } from "../../../materials/instances/material/materialModel";
import { useSceneContext } from "../../../../scene/sceneContext";
const MaterialAnimator = ({
storage,
storage,
}: Readonly<{ storage: StorageUnitStatus }>) => {
const meshRef = useRef<any>(null!);
const { scene } = useThree();
const padding = 0.1;
const { loadingProgress } = useLoadingProgress();
const { scene } = useThree();
const padding = 0.1;
const { loadingProgress } = useLoadingProgress();
const { materialStore } = useSceneContext();
const { materialPositions, setMaterialPositions } = materialStore();
const storageModel = useMemo(() => {
return scene.getObjectByProperty("uuid", storage.modelUuid) as Object3D;
}, [scene, storage.modelUuid, loadingProgress]);
const storageModel = useMemo(() => {
return scene.getObjectByProperty("uuid", storage.modelUuid) as Object3D;
}, [scene, storage.modelUuid, loadingProgress]);
const materialPositions = useMemo(() => {
if (!storageModel || storage.currentMaterials.length === 0) return [];
useEffect(() => {
if (!storageModel || storage.currentMaterials.length === 0) {
setMaterialPositions(storage.modelUuid, []);
return;
}
const box = new Box3().setFromObject(storageModel);
const size = new Vector3();
box.getSize(size);
const box = new Box3().setFromObject(storageModel);
const size = new Vector3();
box.getSize(size);
const matCount = storage.currentMaterials.length;
const materialWidth = 0.45;
const materialDepth = 0.45;
const materialHeight = 0.3;
const cols = Math.floor(size.x / materialWidth);
const rows = Math.floor(size.z / materialDepth);
const itemsPerLayer = cols * rows;
// Assumed size each material needs in world units
const materialWidth = 0.45;
const materialDepth = 0.45;
const materialHeight = 0.3;
const origin = new Vector3(
box.min.x + materialWidth / 2,
box.max.y + padding,
box.min.z + materialDepth / 2
);
const cols = Math.floor(size.x / materialWidth);
const rows = Math.floor(size.z / materialDepth);
const itemsPerLayer = cols * rows;
const newMaterials = storage.currentMaterials.map((mat, i) => {
const layer = Math.floor(i / itemsPerLayer);
const layerIndex = i % itemsPerLayer;
const row = Math.floor(layerIndex / cols);
const col = layerIndex % cols;
const origin = new Vector3(
box.min.x + materialWidth / 2,
box.max.y + padding, // slightly above the surface
box.min.z + materialDepth / 2
const position = new Vector3(
origin.x + col * materialWidth,
origin.y + layer * (materialHeight + padding),
origin.z + row * materialDepth
);
return {
materialId: mat.materialId,
position,
};
});
setMaterialPositions(storage.modelUuid, newMaterials);
}, [storageModel, storage.currentMaterials]);
return (
<group position={[0, -padding, 0]}>
{(materialPositions[storage.modelUuid] || []).map(({ materialId, position }) => {
const mat = storage.currentMaterials.find((m) => m.materialId === materialId);
return (
<MaterialModel
key={materialId}
materialId={materialId}
matRef={null}
materialType={mat?.materialType ?? "Default material"}
position={position}
/>
);
})}
</group>
);
return Array.from({ length: matCount }, (_, i) => {
const layer = Math.floor(i / itemsPerLayer);
const layerIndex = i % itemsPerLayer;
const row = Math.floor(layerIndex / cols);
const col = layerIndex % cols;
return new Vector3(
origin.x + col * materialWidth,
origin.y + layer * (materialHeight + padding),
origin.z + row * materialDepth
);
});
}, [storageModel, storage.currentMaterials]);
return (
<group {...{ position: [0, -padding, 0] }}>
{storage.currentMaterials.map((mat, index) => (
<MaterialModel
key={`${index}-${mat.materialId}`}
materialId={mat.materialId}
matRef={meshRef}
materialType={mat.materialType ?? "Default material"}
position={materialPositions[index]}
/>
))}
</group>
);
};
export default MaterialAnimator;

View File

@@ -7,7 +7,6 @@ import { useViewSceneStore } from "../../../../store/builder/store";
function StorageUnitInstances() {
const { storageUnitStore } = useSceneContext();
const { storageUnits } = storageUnitStore();
// console.log('storageUnits: ', storageUnits);
const { viewSceneLabels } = useViewSceneStore();
return (

View File

@@ -1,4 +1,3 @@
import React from 'react'
import StorageUnitInstances from './instances/storageUnitInstances'
function StorageUnit() {

View File

@@ -7,9 +7,10 @@ import { useMachineEventManager } from '../../machine/eventManager/useMachineEve
import { useSceneContext } from '../../../scene/sceneContext';
import { useProductContext } from '../../products/productContext';
import { useHumanEventManager } from '../../human/eventManager/useHumanEventManager';
import { useCraneEventManager } from '../../crane/eventManager/useCraneEventManager';
export function useTriggerHandler() {
const { materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, humanStore, storageUnitStore, productStore } = useSceneContext();
const { materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, humanStore, craneStore, storageUnitStore, productStore } = useSceneContext();
const { selectedProductStore } = useProductContext();
const { handleAction } = useActionHandler();
const { selectedProduct } = selectedProductStore();
@@ -21,8 +22,10 @@ export function useTriggerHandler() {
const { addVehicleToMonitor } = useVehicleEventManager();
const { addMachineToMonitor } = useMachineEventManager();
const { addHumanToMonitor } = useHumanEventManager();
const { addCraneToMonitor } = useCraneEventManager();
const { getVehicleById } = vehicleStore();
const { getHumanById, setHumanScheduled } = humanStore();
const { getCraneById, setCraneScheduled, addCurrentAction } = craneStore();
const { getMachineById, setMachineActive } = machineStore();
const { getStorageUnitById } = storageUnitStore();
const { getMaterialById, setCurrentLocation, setNextLocation, setPreviousLocation, setIsPaused, setIsVisible, setEndTime } = materialStore();
@@ -370,40 +373,78 @@ export function useTriggerHandler() {
}
}
} else {
if (human.isActive === false && human.state === 'idle') {
// Handle current action from arm bot
setIsPaused(materialId, true);
handleAction(action, materialId);
} else {
// Handle current action using Event Manager
setHumanScheduled(human.modelUuid, true);
setIsPaused(materialId, true);
addHumanToMonitor(human.modelUuid, () => {
handleAction(action, materialId)
}, action.actionUuid);
}
}
} else {
if (human.isActive === false && human.state === 'idle') {
// Handle current action from arm bot
setIsPaused(materialId, true);
setHumanScheduled(human.modelUuid, true);
handleAction(action, materialId);
} else {
// Handle current action using Event Manager
setIsPaused(materialId, true);
setHumanScheduled(human.modelUuid, true);
addHumanToMonitor(human.modelUuid, () => {
handleAction(action, materialId)
}, action.actionUuid);
}
} else {
setHumanScheduled(human.modelUuid, true);
setIsPaused(materialId, true);
addHumanToMonitor(human.modelUuid, () => {
handleAction(action, materialId)
}, action.actionUuid);
}
}
}
}
}
}
} else if (toEvent?.type === 'crane') {
// Transfer to Human
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
const material = getMaterialById(materialId);
if (material) {
// Handle current action of the material
handleAction(action, materialId);
if (material.next) {
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
const crane = getCraneById(trigger.triggeredAsset?.triggeredModel.modelUuid);
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: material.next.modelUuid,
pointUuid: material.next.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
setNextLocation(material.materialId, null);
if (action) {
if (crane) {
if (action && action.triggers.length > 0 &&
action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid &&
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid) {
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
if (model?.type === 'human') {
const crane = getCraneById(trigger.triggeredAsset?.triggeredModel.modelUuid);
if (crane) {
setIsPaused(materialId, true);
setCraneScheduled(crane.modelUuid, true);
const human = getHumanById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
if (human) {
addHumanToMonitor(human.modelUuid, () => {
addCurrentAction(crane.modelUuid, action.actionUuid, null, null);
addCraneToMonitor(crane.modelUuid, () => {
setIsPaused(materialId, true);
handleAction(action, materialId);
}, action.actionUuid)
}, action.triggers[0]?.triggeredAsset.triggeredAction.actionUuid)
}
}
}
}
}
}
@@ -580,6 +621,66 @@ export function useTriggerHandler() {
}
}
}
} else if (toEvent?.type === 'crane') {
// Vehicle to Human
if (materialId && trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint && trigger.triggeredAsset.triggeredAction) {
const material = getMaterialById(materialId);
if (material) {
// Handle current action of the material
handleAction(action, materialId);
if (material.next) {
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
const crane = getCraneById(trigger.triggeredAsset?.triggeredModel.modelUuid);
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: material.next.modelUuid,
pointUuid: material.next.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
setNextLocation(material.materialId, null);
if (action) {
if (crane) {
if (action && action.triggers.length > 0 &&
action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid &&
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid) {
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
if (model?.type === 'human') {
const crane = getCraneById(trigger.triggeredAsset?.triggeredModel.modelUuid);
if (crane) {
setIsPaused(materialId, true);
setCraneScheduled(crane.modelUuid, true);
const human = getHumanById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '');
if (human) {
addHumanToMonitor(human.modelUuid, () => {
addCurrentAction(crane.modelUuid, action.actionUuid, null, null);
addCraneToMonitor(crane.modelUuid, () => {
setIsPaused(materialId, true);
handleAction(action, materialId);
}, action.actionUuid)
}, action.triggers[0]?.triggeredAsset.triggeredAction.actionUuid)
}
}
}
}
}
}
}
}
}
}
} else if (fromEvent?.type === 'machine') {
if (toEvent?.type === 'transfer') {
@@ -1260,12 +1361,6 @@ export function useTriggerHandler() {
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
});
setIsPaused(material.materialId, true);
setIsVisible(material.materialId, true);
@@ -1279,6 +1374,13 @@ export function useTriggerHandler() {
if (model?.type === 'roboticArm') {
addArmBotToMonitor(model.modelUuid, () => {
setCurrentLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
});
setNextLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid || '',
@@ -1288,6 +1390,13 @@ export function useTriggerHandler() {
})
} else if (model?.type === 'vehicle') {
addVehicleToMonitor(model.modelUuid, () => {
setCurrentLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
});
setNextLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid || '',
@@ -1296,6 +1405,13 @@ export function useTriggerHandler() {
setIsPaused(material.materialId, false);
})
} else if (model?.type === 'transfer') {
setCurrentLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
});
setNextLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid || '',
@@ -1303,15 +1419,48 @@ export function useTriggerHandler() {
setIsPaused(material.materialId, false);
} else if (model?.type === 'human') {
addHumanToMonitor(model.modelUuid, () => {
setNextLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid || '',
})
if (fromEvent.modelUuid === model.modelUuid) {
setIsPaused(material.materialId, false);
}, action.actionUuid)
setCurrentLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
});
setTimeout(() => {
setIsPaused(material.materialId, false);
setNextLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid || '',
})
}, 0)
} else {
addHumanToMonitor(model.modelUuid, () => {
setCurrentLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
});
setNextLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid || '',
})
setIsPaused(material.materialId, false);
}, action.actionUuid)
}
} else {
setCurrentLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
});
setNextLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid || '',
@@ -1320,6 +1469,134 @@ export function useTriggerHandler() {
setIsPaused(material.materialId, false);
}
}
} else if (material && action.actionType === 'operator') {
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: trigger.triggeredAsset.triggeredModel.modelUuid,
pointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
const action = getActionByUuid(selectedProduct.productUuid, trigger.triggeredAsset.triggeredAction.actionUuid);
if (action && action.triggers.length > 0 &&
action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid &&
action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid &&
action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid) {
const model = getEventByModelUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
if (model?.type === 'roboticArm') {
handleAction(action, material.materialId);
} else if (model?.type === 'vehicle') {
const nextAction = getActionByUuid(selectedProduct.productUuid, action.triggers[0]?.triggeredAsset?.triggeredAction.actionUuid);
if (action) {
handleAction(action, material.materialId);
const vehicle = getVehicleById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: trigger.triggeredAsset?.triggeredModel.modelUuid,
pointUuid: trigger.triggeredAsset?.triggeredPoint?.pointUuid,
actionUuid: trigger.triggeredAsset?.triggeredAction?.actionUuid,
});
setNextLocation(material.materialId, null);
if (nextAction) {
if (vehicle) {
if (vehicle.isActive === false && vehicle.state === 'idle' && vehicle.isPicking && vehicle.currentLoad < vehicle.point.action.loadCapacity) {
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid || '',
actionUuid: action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid || '',
});
setNextLocation(material.materialId, null);
setIsVisible(materialId, false);
setIsPaused(materialId, false);
// Handle current action from vehicle
handleAction(nextAction, materialId);
} else {
// Handle current action using Event Manager
setIsPaused(materialId, true);
addVehicleToMonitor(vehicle.modelUuid, () => {
setPreviousLocation(material.materialId, {
modelUuid: material.current.modelUuid,
pointUuid: material.current.pointUuid,
actionUuid: material.current.actionUuid,
})
setCurrentLocation(material.materialId, {
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || '',
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid || '',
actionUuid: action.triggers[0]?.triggeredAsset?.triggeredAction?.actionUuid || '',
});
setNextLocation(material.materialId, null);
setIsPaused(materialId, false);
setIsVisible(materialId, false);
handleAction(nextAction, materialId);
})
}
}
}
}
} else if (model?.type === 'transfer') {
const conveyor = getConveyorById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid);
if (conveyor) {
setNextLocation(material.materialId, {
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid,
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid,
})
setIsPaused(material.materialId, false);
setIsVisible(material.materialId, true);
handleAction(action, material.materialId);
}
} else {
setNextLocation(material.materialId, {
modelUuid: action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid,
pointUuid: action.triggers[0]?.triggeredAsset?.triggeredPoint?.pointUuid,
})
handleAction(action, material.materialId);
}
} else if (action) {
setNextLocation(material.materialId, null)
handleAction(action, material.materialId);
}
}
}
@@ -1521,6 +1798,29 @@ export function useTriggerHandler() {
} else if (toEvent?.type === 'human') {
// Human to Human
}
} else if (fromEvent?.type === 'crane') {
if (toEvent?.type === 'transfer') {
// Crane Unit to Transfer
} else if (toEvent?.type === 'vehicle') {
// Crane Unit to Vehicle
} else if (toEvent?.type === 'machine') {
// Crane Unit to Machine
} else if (toEvent?.type === 'roboticArm') {
// Crane Unit to Robotic Arm
} else if (toEvent?.type === 'storageUnit') {
// Crane Unit to Storage Unit
} else if (toEvent?.type === 'human') {
// Crane Unit to Human
} else if (toEvent?.type === 'crane') {
// Crane Unit to Human
}
}
}

View File

@@ -13,30 +13,44 @@ const MaterialAnimator = ({ agvDetail }: { agvDetail: VehicleStatus }) => {
useEffect(() => {
const loadState = agvDetail.currentLoad > 0;
setHasLoad(loadState);
if (!loadState) {
setIsAttached(false);
if (meshRef.current?.parent) {
meshRef.current.parent.remove(meshRef.current);
const agvModel = scene.getObjectByProperty("uuid", agvDetail.modelUuid) as THREE.Object3D;
if (agvModel) {
const material = agvModel.getObjectByName('Sample-Material');
if (material) {
agvModel.remove(material);
}
}
}
}, [agvDetail.currentLoad]);
useFrame(() => {
// if (agvDetail.currentMaterials.length === 0 || agvDetail.currentLoad === 0) {
// const agvModel = scene.getObjectByProperty("uuid", agvDetail.modelUuid) as THREE.Object3D;
// if (agvModel) {
// const material = agvModel.getObjectByName('Sample-Material');
// if (material) {
// agvModel.remove(material);
// }
// }
// }
if (!hasLoad || !meshRef.current || isAttached) return;
const agvModel = scene.getObjectByProperty("uuid", agvDetail.modelUuid) as THREE.Object3D;
if (agvModel && !isAttached) {
if (meshRef.current.parent) {
meshRef.current.parent.remove(meshRef.current);
const material = agvModel.getObjectByName('Sample-Material');
if (material) {
agvModel.remove(material);
}
agvModel.add(meshRef.current);
meshRef.current.position.copy(offset);
meshRef.current.rotation.set(0, 0, 0);
meshRef.current.scale.set(1, 1, 1);
setIsAttached(true);
}
});
@@ -45,6 +59,7 @@ const MaterialAnimator = ({ agvDetail }: { agvDetail: VehicleStatus }) => {
<>
{hasLoad && agvDetail.currentMaterials.length > 0 && (
<MaterialModel
name={'Sample-Material'}
matRef={meshRef}
materialId={agvDetail.currentMaterials[0].materialId || ''}
materialType={agvDetail.currentMaterials[0].materialType || 'Default material'}

View File

@@ -1,12 +1,12 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useFrame, useThree, ThreeEvent } from '@react-three/fiber';
import * as THREE from 'three';
import { Line } from '@react-three/drei';
import { RapierRigidBody } from '@react-three/rapier';
import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore';
import { useSceneContext } from '../../../../scene/sceneContext';
import { useActiveTool, useSelectedPath } from '../../../../../store/builder/store';
import { RapierRigidBody } from '@react-three/rapier';
interface VehicleAnimatorProps {
path: [number, number, number][];
@@ -236,7 +236,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai
return (
<>
{selectedPath === "auto" &&
<group>
<group visible={false}>
{currentPath.map((pos, i) => {
if (i < currentPath.length - 1) {
return (

View File

@@ -12,24 +12,26 @@ import MaterialAnimator from '../animator/materialAnimator';
import VehicleAnimator from '../animator/vehicleAnimator';
import { useHumanEventManager } from '../../../human/eventManager/useHumanEventManager';
import { useCraneEventManager } from '../../../crane/eventManager/useCraneEventManager';
function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>) {
const { navMesh } = useNavMesh();
const { isPlaying } = usePlayButtonStore();
const { materialStore, armBotStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, productStore, assetStore } = useSceneContext();
const { materialStore, armBotStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, productStore, assetStore } = useSceneContext();
const { removeMaterial, setEndTime, setIsVisible } = materialStore();
const { getStorageUnitById } = storageUnitStore();
const { getHumanById, addCurrentAction, addCurrentMaterial, incrementHumanLoad } = humanStore();
const { getCraneById, addCurrentAction: addCraneAction, addCurrentMaterial: addCraneMaterial, incrementCraneLoad } = craneStore();
const { getArmBotById } = armBotStore();
const { getConveyorById } = conveyorStore();
const { triggerPointActions } = useTriggerHandler();
const { setCurrentAnimation, getAssetById } = assetStore();
const { addHumanToMonitor } = useHumanEventManager();
const { addCraneToMonitor } = useCraneEventManager();
const { getActionByUuid, getEventByModelUuid, getTriggerByUuid } = productStore();
const { selectedProductStore } = useProductContext();
const { selectedProduct } = selectedProductStore();
const { vehicles, setVehicleActive, setVehicleState, setVehiclePicking, clearCurrentMaterials, setVehicleLoad, decrementVehicleLoad, removeLastMaterial, getLastMaterial, incrementIdleTime, incrementActiveTime, resetTime } = vehicleStore();
const [currentPhase, setCurrentPhase] = useState<string>('stationed');
const { vehicles, setCurrentPhase, setVehicleActive, setVehicleState, setVehiclePicking, clearCurrentMaterials, setVehicleLoad, decrementVehicleLoad, removeLastMaterial, getLastMaterial, incrementIdleTime, incrementActiveTime, resetTime } = vehicleStore();
const [path, setPath] = useState<[number, number, number][]>([]);
const pauseTimeRef = useRef<number | null>(null);
const idleTimeRef = useRef<number>(0);
@@ -80,7 +82,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
// Function to reset everything
function reset() {
setCurrentPhase('stationed');
setCurrentPhase(agvDetail.modelUuid, 'stationed');
setVehicleActive(agvDetail.modelUuid, false);
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleState(agvDetail.modelUuid, 'idle');
@@ -103,20 +105,20 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
if (isPlaying && selectedPath === "auto") {
if (!agvDetail.point.action.unLoadPoint || !agvDetail.point.action.pickUpPoint) return;
if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'stationed') {
if (!agvDetail.isActive && agvDetail.state === 'idle' && agvDetail.currentPhase === 'stationed') {
const toPickupPath = computePath(
new THREE.Vector3(agvDetail?.position[0], agvDetail?.position[1], agvDetail?.position[2]),
agvDetail?.point?.action?.pickUpPoint?.position
);
setPath(toPickupPath);
setCurrentPhase('stationed-pickup');
setCurrentPhase(agvDetail.modelUuid, 'stationed-pickup');
setVehicleState(agvDetail.modelUuid, 'running');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, true);
vehicleStatus(agvDetail.modelUuid, 'Started from station, heading to pickup');
return;
} else if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'picking') {
} else if (!agvDetail.isActive && agvDetail.state === 'idle' && agvDetail.currentPhase === 'picking') {
if (agvDetail.currentLoad === agvDetail.point.action.loadCapacity && agvDetail.currentMaterials.length > 0) {
if (agvDetail.point.action.pickUpPoint && agvDetail.point.action.unLoadPoint) {
const toDrop = computePath(
@@ -124,21 +126,21 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
agvDetail.point.action.unLoadPoint.position
);
setPath(toDrop);
setCurrentPhase('pickup-drop');
setCurrentPhase(agvDetail.modelUuid, 'pickup-drop');
setVehicleState(agvDetail.modelUuid, 'running');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, true);
vehicleStatus(agvDetail.modelUuid, 'Started from pickup point, heading to drop point');
}
}
} else if (!agvDetail.isActive && agvDetail.state === 'idle' && currentPhase === 'dropping' && agvDetail.currentLoad === 0) {
} else if (!agvDetail.isActive && agvDetail.state === 'idle' && agvDetail.currentPhase === 'dropping' && agvDetail.currentLoad === 0) {
if (agvDetail.point.action.pickUpPoint && agvDetail.point.action.unLoadPoint) {
const dropToPickup = computePath(
agvDetail.point.action.unLoadPoint.position,
agvDetail.point.action.pickUpPoint.position
);
setPath(dropToPickup);
setCurrentPhase('drop-pickup');
setCurrentPhase(agvDetail.modelUuid, 'drop-pickup');
setVehicleState(agvDetail.modelUuid, 'running');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, true);
@@ -149,7 +151,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
else {
reset()
}
}, [vehicles, currentPhase, path, isPlaying, selectedPath]);
}, [vehicles, agvDetail.currentPhase, path, isPlaying, selectedPath]);
function animate(currentTime: number) {
if (previousTimeRef.current === null) {
@@ -198,22 +200,22 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
}, [agvDetail, isPlaying]);
function handleCallBack() {
if (currentPhase === 'stationed-pickup') {
setCurrentPhase('picking');
if (agvDetail.currentPhase === 'stationed-pickup') {
setCurrentPhase(agvDetail.modelUuid, 'picking');
setVehicleState(agvDetail.modelUuid, 'idle');
setVehiclePicking(agvDetail.modelUuid, true);
setVehicleActive(agvDetail.modelUuid, false);
vehicleStatus(agvDetail.modelUuid, 'Reached pickup point, waiting for material');
setPath([]);
} else if (currentPhase === 'pickup-drop') {
setCurrentPhase('dropping');
} else if (agvDetail.currentPhase === 'pickup-drop') {
setCurrentPhase(agvDetail.modelUuid, 'dropping');
setVehicleState(agvDetail.modelUuid, 'idle');
setVehiclePicking(agvDetail.modelUuid, false);
setVehicleActive(agvDetail.modelUuid, false);
vehicleStatus(agvDetail.modelUuid, 'Reached drop point');
setPath([]);
} else if (currentPhase === 'drop-pickup') {
setCurrentPhase('picking');
} else if (agvDetail.currentPhase === 'drop-pickup') {
setCurrentPhase(agvDetail.modelUuid, 'picking');
setVehicleState(agvDetail.modelUuid, 'idle');
setVehiclePicking(agvDetail.modelUuid, true);
setVehicleActive(agvDetail.modelUuid, false);
@@ -252,6 +254,12 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
if (action && (triggeredAction?.actionType === 'assembly' || triggeredAction?.actionType === 'worker')) {
handleMaterialDropToHuman(model, triggeredAction);
}
} else if (model.type === 'crane') {
const action = getActionByUuid(selectedProduct.productUuid, agvDetail.point.action.actionUuid);
if (action && (triggeredAction?.actionType === 'pickAndDrop')) {
handleMaterialDropToCrane(model, triggeredAction);
addCraneAction(model.modelUuid, triggeredAction.actionUuid, null, null);
}
}
} else {
const droppedMaterial = agvDetail.currentLoad;
@@ -265,6 +273,64 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
}
}
function handleMaterialDropToCrane(model: CraneEventSchema, action: CraneAction) {
if (model) {
if (action.actionType === 'pickAndDrop') {
addCraneToMonitor(model.modelUuid, () => {
loopMaterialDropToCrane(
agvDetail,
model.modelUuid,
action.actionUuid
);
}, action.actionUuid || '')
}
}
}
function loopMaterialDropToCrane(
vehicle: VehicleStatus,
craneId: string,
craneActionId: string
) {
let currentVehicleLoad = vehicle.currentLoad;
const unloadLoop = () => {
const crane = getCraneById(craneId);
const craneaction = crane?.point.actions.find((action) => action.actionUuid === craneActionId);
if (!crane || crane.currentAction?.actionUuid !== craneaction?.actionUuid) return;
if (crane.isCarrying) {
decrementVehicleLoad(vehicle.modelUuid, 1);
currentVehicleLoad -= 1;
const material = removeLastMaterial(vehicle.modelUuid);
if (material) {
setIsVisible(material.materialId, false);
}
return;
} else if (!crane.isCarrying && !crane.isActive && crane.currentLoad < (craneaction?.maxPickUpCount || 0) && craneaction?.actionType === 'pickAndDrop') {
const material = getLastMaterial(vehicle.modelUuid);
if (material) {
incrementCraneLoad(craneId, 1);
addCraneAction(craneId, craneActionId, material.materialType, material.materialId);
addCraneMaterial(craneId, material.materialType, material.materialId);
}
}
setTimeout(() => {
requestAnimationFrame(unloadLoop);
}, 500)
};
const crane = getCraneById(craneId);
const craneaction = crane?.point.actions.find((action) => action.actionUuid === craneActionId);
if (crane && crane.currentLoad < (craneaction?.maxPickUpCount || 0)) {
setTimeout(() => {
unloadLoop();
}, 500)
}
}
function handleMaterialDropToHuman(model: HumanEventSchema, action: HumanAction) {
if (model) {
@@ -576,7 +642,7 @@ function VehicleInstance({ agvDetail }: Readonly<{ agvDetail: VehicleStatus }>)
<VehicleAnimator
path={path}
handleCallBack={handleCallBack}
currentPhase={currentPhase}
currentPhase={agvDetail.currentPhase}
agvUuid={agvDetail?.modelUuid}
agvDetail={agvDetail}
reset={reset}

View File

@@ -34,7 +34,7 @@ const Project: React.FC = () => {
const { setUserName } = useUserName();
const { setOrganization } = useOrganization();
const { projectId } = useParams();
const { setProjectName } = useProjectName();
const { projectName, setProjectName } = useProjectName();
const { userId, email, organization, userName } = getUserData();
const { selectedUser } = useSelectedUserStore();
const { isLogListVisible } = useLogger();
@@ -57,7 +57,6 @@ const Project: React.FC = () => {
const matchedProject = allProjects.find(
(val: any) => val.projectUuid === projectId || val._id === projectId
);
if (matchedProject) {
setProjectName(matchedProject.projectName);
await viewProject(organization, matchedProject._id, userId);

View File

@@ -202,10 +202,10 @@ const UserAuth: React.FC = () => {
</div>
{!isSignIn && (
<div className="policy-checkbox">
<input type="checkbox" name="" id="" required />
<div className="label">
<input type="checkbox" id="tos" required />
<label htmlFor="tos" className="label">
I have read and agree to the terms of service
</div>
</label>
</div>
)}
<button id="form-submit" type="submit" className="continue-button">

View File

@@ -1,6 +1,7 @@
let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`;
export const duplicateProject = async (
refProjectID: string,
projectUuid: string,
thumbnail: string,
projectName: string
@@ -16,27 +17,32 @@ export const duplicateProject = async (
token: localStorage.getItem("token") || "", // Coerce null to empty string
refresh_token: localStorage.getItem("refreshToken") || "",
},
body: JSON.stringify({ projectUuid, thumbnail, projectName }),
body: JSON.stringify({
refProjectID,
projectUuid,
thumbnail,
projectName,
}),
}
);
const newAccessToken = response.headers.get("x-access-token");
if (newAccessToken) {
//console.log("New token received:", newAccessToken);
//
localStorage.setItem("token", newAccessToken);
}
// console.log("response: ", response);
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");
}
}
};

Some files were not shown because too many files have changed in this diff Show More