diff --git a/app/src/components/Dashboard/DashboardCard.tsx b/app/src/components/Dashboard/DashboardCard.tsx index 68d91e7..0f75d19 100644 --- a/app/src/components/Dashboard/DashboardCard.tsx +++ b/app/src/components/Dashboard/DashboardCard.tsx @@ -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; - handleTrashDeleteProject?: (projectId: string) => Promise; - handleRestoreProject?: (projectId: string) => Promise; - handleDuplicateWorkspaceProject?: ( - projectId: string, - projectName: string, - thumbnail: string, - userId?: string - ) => Promise; - handleDuplicateRecentProject?: ( - projectId: string, - projectName: string, - thumbnail: string - ) => Promise; - active?: string; - setIsSearchActive?: React.Dispatch>; - setRecentDuplicateData?: React.Dispatch>; - setProjectDuplicateData?: React.Dispatch>; - setActiveFolder?: React.Dispatch>; + projectName: string; + thumbnail: any; + projectId: string; + createdAt?: string; + isViewed?: string; + createdBy?: { _id: string; userName: string }; + handleDeleteProject?: (projectId: string) => Promise; + handleTrashDeleteProject?: (projectId: string) => Promise; + handleRestoreProject?: (projectId: string) => Promise; + handleDuplicateWorkspaceProject?: ( + projectId: string, + projectName: string, + thumbnail: string, + userId?: string + ) => Promise; + handleDuplicateRecentProject?: ( + projectId: string, + projectName: string, + thumbnail: string + ) => Promise; + active?: string; + setIsSearchActive?: React.Dispatch>; + setRecentDuplicateData?: React.Dispatch>; + setProjectDuplicateData?: React.Dispatch>; + setActiveFolder?: React.Dispatch>; } type RelativeTimeFormatUnit = any; const DashboardCard: React.FC = ({ - 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(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(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 = { - 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 = { - 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 = { + 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 ( - - - - - {isKebabOpen && ( -
- {getOptions().map((option) => ( - - ))} -
+
+ {isRenaming ? ( + { + e.stopPropagation(); + handleProjectName(e.target.value); + }} + onBlur={() => { + setIsRenaming(false); + setProjectName(renameValue); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + setProjectName(renameValue); + setIsRenaming(false); + } + }} + autoFocus + /> + ) : ( + {renameValue} )} - - ); + {createdAt && ( +
+ {active && active == "trash" ? `Trashed by you` : `Edited `}{" "} + {getRelativeTime(createdAt)} +
+ )} +
+
+
+ {!createdBy + ? userName + ? userName?.charAt(0).toUpperCase() + : "A" + : createdBy?.userName?.charAt(0).toUpperCase()} +
+ +
+ + + {isKebabOpen && ( +
+ {getOptions().map((option) => ( + + ))} +
+ )} + + ); }; -export default DashboardCard; \ No newline at end of file +export default DashboardCard; diff --git a/app/src/components/Dashboard/DashboardHome.tsx b/app/src/components/Dashboard/DashboardHome.tsx index 6e9dce7..0569c08 100644 --- a/app/src/components/Dashboard/DashboardHome.tsx +++ b/app/src/components/Dashboard/DashboardHome.tsx @@ -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({}); + const [isSearchActive, setIsSearchActive] = useState(false); const { userId, organization } = getUserData(); const { projectSocket } = useSocketStore(); const [recentDuplicateData, setRecentDuplicateData] = useState({}); - 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; \ No newline at end of file +export default DashboardHome; diff --git a/app/src/components/Dashboard/DashboardProjects.tsx b/app/src/components/Dashboard/DashboardProjects.tsx index ccb56b2..ba2db7b 100644 --- a/app/src/components/Dashboard/DashboardProjects.tsx +++ b/app/src/components/Dashboard/DashboardProjects.tsx @@ -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( {} ); - const [sharedwithMeProject, setSharedWithMeProjects] = useState([]) + const [sharedwithMeProject, setSharedWithMeProjects] = useState([]); const [projectDuplicateData, setProjectDuplicateData] = useState({}); const [isSearchActive, setIsSearchActive] = useState(false); const [activeFolder, setActiveFolder] = useState("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 (
@@ -184,7 +188,10 @@ const DashboardProjects: React.FC = () => { handleProjectsSearch={handleProjectsSearch} /> -
+
-
{activeFolder == "myProjects" ? renderProjects() : renderSharedProjects()}
+
+ {activeFolder == "myProjects" + ? renderProjects() + : renderSharedProjects()} +
- {projectDuplicateData && Object.keys(projectDuplicateData).length > 0 && ( - - )} + {projectDuplicateData && + Object.keys(projectDuplicateData).length > 0 && ( + + )}
); }; export default DashboardProjects; - - diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx index 252e44d..a6332db 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx @@ -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" && } {assetType === "storageUnit" && } {assetType === "human" && } + {assetType === "crane" && } )} {!currentEventData && selectedEventSphere && ( diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/actions/pillarJibAction.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/pillarJibAction.tsx new file mode 100644 index 0000000..e7ff2c5 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/actions/pillarJibAction.tsx @@ -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 = ({ + loadCount +}) => { + return ( +
+ {loadCount && ( + { }} + onChange={(value) => loadCount.onChange(parseInt(value))} + /> + )} +
+ ); +}; + +export default WorkerAction; \ No newline at end of file diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx new file mode 100644 index 0000000..f5d1229 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/craneMechanics.tsx @@ -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(); + + 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 ( + <> +
+ + + {selectedAction.actionId && currentAction && ( +
+
+ +
+
+ { }} + disabled={true} + /> +
+
+ +
+
+ )} +
+ + ); +} + +export default CraneMechanics \ No newline at end of file diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx index fc73e58..9d8e393 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/humanMechanics.tsx @@ -6,8 +6,8 @@ import RenameInput from "../../../../../ui/inputs/RenameInput"; import LabledDropdown from "../../../../../ui/inputs/LabledDropdown"; import Trigger from "../trigger/Trigger"; import ActionsList from "../components/ActionsList"; -import WorkerAction from "../actions/workerAction"; -import AssemblyAction from "../actions/assemblyAction"; +import WorkerAction from "../actions/WorkerAction"; +import AssemblyAction from "../actions/AssemblyAction"; import { useSelectedEventData, useSelectedAction } from "../../../../../../store/simulation/useSimulationStore"; import { upsertProductOrEventApi } from "../../../../../../services/simulation/products/UpsertProductOrEventApi"; diff --git a/app/src/components/ui/FileMenu.tsx b/app/src/components/ui/FileMenu.tsx index 86146ba..4575b3b 100644 --- a/app/src/components/ui/FileMenu.tsx +++ b/app/src/components/ui/FileMenu.tsx @@ -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(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(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 ( - - ); + const updatedProjectName = await updateProject( + projectId, + userId, + organization, + undefined, + projectName + ); + } catch (error) { + console.error("Error updating project name:", error); + } + }; + return ( + + ); }; export default FileMenu; diff --git a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts index c08144c..1fc26ae 100644 --- a/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts +++ b/app/src/modules/builder/asset/models/model/eventHandlers/useEventHandlers.ts @@ -24,17 +24,17 @@ export function useModelEventHandlers({ boundingBox: THREE.Box3 | null, groupRef: React.RefObject, }) { - const { controls, gl } = useThree(); + const { controls, gl, camera } = useThree(); const { activeTool } = useActiveTool(); const { activeModule } = useModuleStore(); const { toggleView } = useToggleView(); const { subModule } = useSubModuleStore(); const { socket } = useSocketStore(); - const { eventStore, productStore, assetStore } = useSceneContext(); + const { eventStore, productStore, assetStore, undoRedo3DStore } = useSceneContext(); + const { push3D } = undoRedo3DStore(); const { removeAsset } = assetStore(); - const { removeEvent } = eventStore(); + const { removeEvent, getEventByModelUuid } = eventStore(); const { getIsEventInProduct, addPoint, deleteEvent } = productStore(); - const { getEventByModelUuid } = eventStore(); const { setSelectedAsset, clearSelectedAsset } = useSelectedAsset(); const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem(); const { setSelectedFloorItem } = useSelectedFloorItem(); @@ -68,25 +68,47 @@ export function useModelEventHandlers({ 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); } @@ -130,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!"); } diff --git a/app/src/modules/builder/asset/models/model/model.tsx b/app/src/modules/builder/asset/models/model/model.tsx index 9767138..3f128bb 100644 --- a/app/src/modules/builder/asset/models/model/model.tsx +++ b/app/src/modules/builder/asset/models/model/model.tsx @@ -8,7 +8,7 @@ import useModuleStore from '../../../../../store/useModuleStore'; import { useSceneContext } from '../../../../scene/sceneContext'; import { SkeletonUtils } from 'three-stdlib'; -import { getAssetIksApi } from '../../../../../services/simulation/ik/getAssetIKs'; +import { getAssetFieldApi } from '../../../../../services/factoryBuilder/asset/floorAsset/getAssetField'; import { ModelAnimator } from './animator/modelAnimator'; import { useModelEventHandlers } from './eventHandlers/useEventHandlers'; @@ -26,19 +26,31 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere const [boundingBox, setBoundingBox] = useState(null); const [isSelected, setIsSelected] = useState(false); const groupRef = useRef(null); - const [ikData, setIkData] = useState(); + const [fieldData, setFieldData] = useState(); const { selectedAssets } = useSelectedAssets(); 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; + setFieldData(fieldData); + } + } else if (data.type === 'Crane') { + if (data.data) { + const fieldData = data.data; + setFieldData(fieldData); + } } }) } - }, [asset.modelUuid, ikData]) + }, [asset.modelUuid, fieldData]) useEffect(() => { setDeletableFloorItem(null); @@ -157,7 +169,7 @@ function Model({ asset, isRendered, loader }: { readonly asset: Asset, isRendere position={asset.position} rotation={asset.rotation} visible={asset.isVisible} - userData={{ ...asset, iks: ikData }} + userData={{ ...asset, fieldData: fieldData }} castShadow receiveShadow onDoubleClick={(e) => { diff --git a/app/src/modules/scene/controls/controls.tsx b/app/src/modules/scene/controls/controls.tsx index 354d2bd..c41a4bd 100644 --- a/app/src/modules/scene/controls/controls.tsx +++ b/app/src/modules/scene/controls/controls.tsx @@ -17,6 +17,7 @@ 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(null); @@ -145,6 +146,8 @@ export default function Controls() { + + diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/copyPasteControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/copyPasteControls3D.tsx index b9056d7..bb9682e 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/copyPasteControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/copyPasteControls3D.tsx @@ -28,7 +28,8 @@ 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, updateAsset, removeAsset, getAssetById } = assetStore(); @@ -209,6 +210,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; @@ -540,9 +544,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(); }; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx index 153ff07..7afa982 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/duplicationControls3D.tsx @@ -26,7 +26,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(); @@ -207,6 +208,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; @@ -538,9 +542,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(); }; diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx index 2a8146c..234c345 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/moveControls3D.tsx @@ -37,7 +37,8 @@ function MoveControls3D({ 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(); @@ -292,6 +293,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; @@ -299,6 +303,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, @@ -376,6 +406,27 @@ 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(); diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx index 7a8f7d4..d3457e2 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/rotateControls3D.tsx @@ -32,7 +32,8 @@ 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(); @@ -227,12 +228,43 @@ function RotateControls3D({ 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, @@ -310,6 +342,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]); diff --git a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx index 9838189..4dd2063 100644 --- a/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx +++ b/app/src/modules/scene/controls/selectionControls/selection3D/selectionControls3D.tsx @@ -31,9 +31,10 @@ const SelectionControls3D: React.FC = () => { const boundingBoxRef = useRef(); 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(); @@ -274,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 @@ -329,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) => { diff --git a/app/src/modules/scene/controls/transformControls/transformControls.tsx b/app/src/modules/scene/controls/transformControls/transformControls.tsx index e0ce1cf..415be9b 100644 --- a/app/src/modules/scene/controls/transformControls/transformControls.tsx +++ b/app/src/modules/scene/controls/transformControls/transformControls.tsx @@ -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) && { 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; \ No newline at end of file +export default use2DUndoHandler; \ No newline at end of file diff --git a/app/src/modules/scene/controls/undoRedoControls/handlers/use3DRedoHandler.ts b/app/src/modules/scene/controls/undoRedoControls/handlers/use3DRedoHandler.ts new file mode 100644 index 0000000..b44f6a7 --- /dev/null +++ b/app/src/modules/scene/controls/undoRedoControls/handlers/use3DRedoHandler.ts @@ -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; diff --git a/app/src/modules/scene/controls/undoRedoControls/handlers/use3DUndoHandler.ts b/app/src/modules/scene/controls/undoRedoControls/handlers/use3DUndoHandler.ts new file mode 100644 index 0000000..50fd6cf --- /dev/null +++ b/app/src/modules/scene/controls/undoRedoControls/handlers/use3DUndoHandler.ts @@ -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; \ No newline at end of file diff --git a/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx b/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx index ecd70e1..1965087 100644 --- a/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx +++ b/app/src/modules/scene/controls/undoRedoControls/undoRedo2D/undoRedo2DControls.tsx @@ -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(); diff --git a/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx b/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx new file mode 100644 index 0000000..9331320 --- /dev/null +++ b/app/src/modules/scene/controls/undoRedoControls/undoRedo3D/undoRedo3DControls.tsx @@ -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; \ No newline at end of file diff --git a/app/src/modules/scene/helpers/StatsHelper.tsx b/app/src/modules/scene/helpers/StatsHelper.tsx new file mode 100644 index 0000000..486488b --- /dev/null +++ b/app/src/modules/scene/helpers/StatsHelper.tsx @@ -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 ? : null; +} diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx index e10fc4a..feddc2c 100644 --- a/app/src/modules/scene/scene.tsx +++ b/app/src/modules/scene/scene.tsx @@ -14,8 +14,9 @@ import { getAllProjects } from "../../services/dashboard/getAllProjects"; import { getUserData } from "../../functions/getUserData"; import { useLoadingProgress, useSocketStore } from "../../store/builder/store"; import { Color, SRGBColorSpace } from "three"; +import StatsHelper from "./helpers/StatsHelper"; -export default function Scene({ layout }: { readonly layout: 'Main Layout' | 'Comparison Layout' }) { +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"] }, @@ -32,28 +33,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); - }); + 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]) + }, [activeModule, assets, loadingProgress]); return ( @@ -62,20 +62,17 @@ 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 }} > + ); diff --git a/app/src/modules/scene/sceneContext.tsx b/app/src/modules/scene/sceneContext.tsx index ac17198..73e5f9c 100644 --- a/app/src/modules/scene/sceneContext.tsx +++ b/app/src/modules/scene/sceneContext.tsx @@ -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'; @@ -31,6 +32,7 @@ type SceneContextValue = { floorStore: FloorStoreType, undoRedo2DStore: UndoRedo2DStoreType, + undoRedo3DStore: UndoRedo3DStoreType, eventStore: EventStoreType, productStore: ProductStoreType, @@ -45,6 +47,7 @@ type SceneContextValue = { craneStore: CraneStoreType; humanEventManagerRef: React.RefObject; + craneEventManagerRef: React.RefObject; clearStores: () => void; @@ -69,6 +72,7 @@ export function SceneProvider({ const floorStore = useMemo(() => createFloorStore(), []); const undoRedo2DStore = useMemo(() => createUndoRedo2DStore(), []); + const undoRedo3DStore = useMemo(() => createUndoRedo3DStore(), []); const eventStore = useMemo(() => createEventStore(), []); const productStore = useMemo(() => createProductStore(), []); @@ -83,6 +87,7 @@ export function SceneProvider({ const craneStore = useMemo(() => createCraneStore(), []); const humanEventManagerRef = useRef({ humanStates: [] }); + const craneEventManagerRef = useRef({ craneStates: [] }); const clearStores = useMemo(() => () => { assetStore.getState().clearAssets(); @@ -92,6 +97,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(); @@ -103,7 +109,8 @@ export function SceneProvider({ humanStore.getState().clearHumans(); craneStore.getState().clearCranes(); humanEventManagerRef.current.humanStates = []; - }, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore]); + craneEventManagerRef.current.craneStates = []; + }, [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, undoRedo2DStore, undoRedo3DStore, floorStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore]); const contextValue = useMemo(() => ( { @@ -114,6 +121,7 @@ export function SceneProvider({ zoneStore, floorStore, undoRedo2DStore, + undoRedo3DStore, eventStore, productStore, materialStore, @@ -125,10 +133,11 @@ export function SceneProvider({ humanStore, craneStore, humanEventManagerRef, + craneEventManagerRef, clearStores, layout } - ), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, clearStores, layout]); + ), [assetStore, wallAssetStore, wallStore, aisleStore, zoneStore, floorStore, undoRedo2DStore, undoRedo3DStore, eventStore, productStore, materialStore, armBotStore, machineStore, conveyorStore, vehicleStore, storageUnitStore, humanStore, craneStore, clearStores, layout]); return ( diff --git a/app/src/modules/simulation/actions/crane/actionHandler/usePickAndDropHandler.ts b/app/src/modules/simulation/actions/crane/actionHandler/usePickAndDropHandler.ts new file mode 100644 index 0000000..48896f4 --- /dev/null +++ b/app/src/modules/simulation/actions/crane/actionHandler/usePickAndDropHandler.ts @@ -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); + addCurrentMaterial(modelUuid, material.materialType, material.materialId); + + pickAndDropLogStatus(material.materialName, `performing pickAndDrop action`); + + }, [getMaterialById]); + + return { + handlePickAndDrop, + }; +} \ No newline at end of file diff --git a/app/src/modules/simulation/actions/crane/useCraneActions.ts b/app/src/modules/simulation/actions/crane/useCraneActions.ts new file mode 100644 index 0000000..aa7de6f --- /dev/null +++ b/app/src/modules/simulation/actions/crane/useCraneActions.ts @@ -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 + }; +} \ No newline at end of file diff --git a/app/src/modules/simulation/actions/useActionHandler.ts b/app/src/modules/simulation/actions/useActionHandler.ts index 7352ae1..99d8225 100644 --- a/app/src/modules/simulation/actions/useActionHandler.ts +++ b/app/src/modules/simulation/actions/useActionHandler.ts @@ -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 () => { diff --git a/app/src/modules/simulation/crane/eventManager/useCraneEventManager.ts b/app/src/modules/simulation/crane/eventManager/useCraneEventManager.ts new file mode 100644 index 0000000..f8d40d9 --- /dev/null +++ b/app/src/modules/simulation/crane/eventManager/useCraneEventManager.ts @@ -0,0 +1,99 @@ +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, craneEventManagerRef } = useSceneContext(); + const { getCraneById, setCurrentPhase, removeCurrentAction } = craneStore(); + const { getAssetById } = assetStore(); + const { getActionByUuid } = 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) { + 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 + }; +} \ No newline at end of file diff --git a/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx b/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx index 95a531c..1b1332d 100644 --- a/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx +++ b/app/src/modules/simulation/crane/instances/animator/pillarJibAnimator.tsx @@ -1,82 +1,79 @@ +import { useEffect, useState } from 'react'; import * as THREE from 'three'; -import { useEffect, useMemo } from 'react'; -import { useThree } from '@react-three/fiber'; +import { useFrame, useThree } from '@react-three/fiber'; +import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore'; +import { useSceneContext } from '../../../../scene/sceneContext'; -function PillarJibAnimator({ crane }: { crane: CraneStatus }) { +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 } = useSceneContext(); + const { resetAsset } = assetStore(); + 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) { - const base = model.getObjectByName('base'); - const trolley = model.getObjectByName('trolley'); - const hook = model.getObjectByName('hook'); + if (!model) return; + const hook = model.getObjectByName('hook'); - if (base && trolley && hook) { - let trolleyDir = 1; - let hookDir = 1; + if (!hook) return; + const hookWorld = new THREE.Vector3(); + hook.getWorldPosition(hookWorld); - const trolleySpeed = 0.01; - const hookSpeed = 0.01; - const rotationSpeed = 0.005; - - const trolleyMinOffset = -1; - const trolleyMaxOffset = 1.75; - const hookMinOffset = 0.25; - const hookMaxOffset = -1.5; - - const originalTrolleyX = trolley.position.x; - const originalHookY = hook.position.y; - - const animate = () => { - if (base) { - base.rotation.y += rotationSpeed; - } - - if (trolley) { - trolley.position.x += trolleyDir * trolleySpeed; - if (trolley.position.x >= originalTrolleyX + trolleyMaxOffset || - trolley.position.x <= originalTrolleyX + trolleyMinOffset) { - trolleyDir *= -1; - } - } - - if (hook) { - hook.position.y += hookDir * hookSpeed; - if (hook.position.y >= originalHookY + hookMinOffset || - hook.position.y <= originalHookY + hookMaxOffset) { - hookDir *= -1; - } - } - - requestAnimationFrame(animate); - }; - - animate(); + if (crane.currentPhase === 'init-pickup') { + if (crane.currentMaterials.length > 0) { + const material = scene.getObjectByProperty('uuid', crane.currentMaterials[0].materialId); + 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) + ] + ); + } } } - }, [crane, scene]); + }, [crane.currentPhase]) - return ( - - ); -} - -export default PillarJibAnimator; - -function PillarJibHelper({ crane }: { crane: CraneStatus }) { - const { scene } = useThree(); - - const { geometry, position } = useMemo(() => { + useEffect(() => { const model = scene.getObjectByProperty('uuid', crane.modelUuid); - if (!model) return { geometry: null, position: null }; + if (!model) return; const base = model.getObjectByName('base'); const trolley = model.getObjectByName('trolley'); const hook = model.getObjectByName('hook'); - if (!base || !trolley || !hook) return { geometry: null, position: null }; + if (!base || !trolley || !hook || !points) return; const baseWorld = new THREE.Vector3(); base.getWorldPosition(baseWorld); @@ -87,47 +84,244 @@ function PillarJibHelper({ crane }: { crane: CraneStatus }) { const hookWorld = new THREE.Vector3(); hook.getWorldPosition(hookWorld); + const trolleyMinOffset = -1; + const trolleyMaxOffset = 1.75; + const hookMinOffset = 0.25; + const hookMaxOffset = -1.5; + const distFromBase = new THREE.Vector2(trolleyWorld.x - baseWorld.x, trolleyWorld.z - baseWorld.z).length(); - const outerRadius = distFromBase + 1.75; - const innerRadius = Math.max(distFromBase - 1, 0.05); - const height = (0.25 - (-1.5)); - const cylinderYPosition = hookWorld.y + (height / 2) + (-1.5 + 0.25) / 2; + 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 shape = new THREE.Shape(); - shape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false); + const yMin = hookWorld.y + hookMaxOffset; + const yMax = hookWorld.y + hookMinOffset; - const hole = new THREE.Path(); - hole.absarc(0, 0, innerRadius, 0, Math.PI * 2, true); - shape.holes.push(hole); + function clampToCylinder(pos: THREE.Vector3) { + const xzDist = new THREE.Vector2(pos.x - baseWorld.x, pos.z - baseWorld.z); + const distance = xzDist.length(); - const extrudeSettings = { - depth: height, - bevelEnabled: false, - steps: 1 - }; + let clampedDistance = THREE.MathUtils.clamp(distance, innerRadius, outerRadius); + if (distance > 0) { + clampedDistance = Math.max(innerRadius, Math.min(distance, outerRadius)); + } - const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); - const position: [number, number, number] = [baseWorld.x, cylinderYPosition, baseWorld.z]; + const clampedXZ = xzDist.normalize().multiplyScalar(clampedDistance); + const y = THREE.MathUtils.clamp(pos.y, yMin, yMax); - return { geometry, position }; - }, [scene, crane.modelUuid]); + return new THREE.Vector3(baseWorld.x + clampedXZ.x, y, baseWorld.z + clampedXZ.y); + } - if (!geometry || !position) return null; + 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 = 0.01 * speed; + const rotationSpeed = 0.005 * speed; + const trolleySpeed = 0.01 * speed; + + switch (animationPhase) { + case 'init-hook-adjust': { + const hookWorld = new THREE.Vector3(); + hook.getWorldPosition(hookWorld); + const direction = Math.sign((clampedPoints[0].y - baseWorld.y) - hookWorld.y); + hook.position.y += direction * hookSpeed; + + if (parseFloat(Math.abs(hookWorld.y - clampedPoints[0].y).toFixed(2)) < 0.05) { + 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 (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[0].clone()); + animationData.targetTrolleyX = localTarget?.x; + setAnimationPhase('init-move-trolley'); + } + break; + } + + case 'init-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; + setAnimationPhase('init-final-hook-adjust'); + } + break; + } + + case 'init-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; + + 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; \ No newline at end of file diff --git a/app/src/modules/simulation/crane/instances/craneInstances.tsx b/app/src/modules/simulation/crane/instances/craneInstances.tsx index bd6a96b..240062c 100644 --- a/app/src/modules/simulation/crane/instances/craneInstances.tsx +++ b/app/src/modules/simulation/crane/instances/craneInstances.tsx @@ -12,7 +12,7 @@ function CraneInstances() { {crane.subType === "pillarJib" && - + } diff --git a/app/src/modules/simulation/crane/instances/helper/pillarJibHelper.tsx b/app/src/modules/simulation/crane/instances/helper/pillarJibHelper.tsx new file mode 100644 index 0000000..8998865 --- /dev/null +++ b/app/src/modules/simulation/crane/instances/helper/pillarJibHelper.tsx @@ -0,0 +1,141 @@ +import { useMemo, 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 +}: { + crane: CraneStatus, + points: [THREE.Vector3, THREE.Vector3] | null; +}) { + const { scene } = useThree(); + const [clampedPoints, setClampedPoints] = useState<[THREE.Vector3, THREE.Vector3]>(); + const [isInside, setIsInside] = useState<[boolean, boolean]>([false, false]); + + const { geometry, position } = useMemo(() => { + const model = scene.getObjectByProperty('uuid', crane.modelUuid); + if (!model) 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 = new THREE.Vector3(); + base.getWorldPosition(baseWorld); + + const trolleyWorld = new THREE.Vector3(); + trolley.getWorldPosition(trolleyWorld); + + const hookWorld = new THREE.Vector3(); + hook.getWorldPosition(hookWorld); + + const trolleyMinOffset = -1; + const trolleyMaxOffset = 1.75; + const hookMinOffset = 0.25; + const 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 ( + <> + + + + + {points && points.map((point, i) => ( + + + + ))} + + {clampedPoints && clampedPoints.map((point, i) => ( + + + + ))} + + ); +} + +export default PillarJibHelper; \ No newline at end of file diff --git a/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx b/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx index bef6a17..c95fb95 100644 --- a/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx +++ b/app/src/modules/simulation/crane/instances/instance/pillarJibInstance.tsx @@ -1,14 +1,61 @@ +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 PillarJibAnimator from '../animator/pillarJibAnimator' +import PillarJibHelper from '../helper/pillarJibHelper' function PillarJibInstance({ crane }: { crane: CraneStatus }) { + const { isPlaying } = usePlayButtonStore(); + const { craneStore, productStore } = useSceneContext(); + const { getActionByUuid, getEventByModelUuid, getTriggerByUuid } = productStore(); + const { getCraneById, setCurrentPhase } = craneStore(); + const { selectedProductStore } = useProductContext(); + const { selectedProduct } = selectedProductStore(); + const [animationPhase, setAnimationPhase] = useState('idle'); + const [points, setPoints] = useState<[THREE.Vector3, THREE.Vector3] | null>(null); + + useEffect(() => { + if (isPlaying) { + const action = getActionByUuid(selectedProduct.productUuid, crane?.currentAction?.actionUuid || ''); + if (!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'); + } + } + }, [crane]) + + const handleAnimationComplete = (action: string) => { + if (action === 'starting') { + setAnimationPhase('first-hook-adjust'); + } else if (action === 'picking') { + setCurrentPhase(crane.modelUuid, 'picking'); + } + } return ( <> - + + + ) } -export default PillarJibInstance \ No newline at end of file +export default PillarJibInstance; \ No newline at end of file diff --git a/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx b/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx index ab6041c..b43c7fa 100644 --- a/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx +++ b/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx @@ -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); }; diff --git a/app/src/modules/simulation/spatialUI/arm/armBotUI.tsx b/app/src/modules/simulation/spatialUI/arm/armBotUI.tsx index dfd500a..b39076e 100644 --- a/app/src/modules/simulation/spatialUI/arm/armBotUI.tsx +++ b/app/src/modules/simulation/spatialUI/arm/armBotUI.tsx @@ -173,8 +173,8 @@ const ArmBotUI = () => { const targetMesh = scene?.getObjectByProperty("uuid", selectedArmBotData?.modelUuid || ''); - const iks = targetMesh?.userData?.iks; - const firstIK = Array.isArray(iks) && iks.length > 0 ? iks[0] : {}; + const fieldData = targetMesh?.userData?.fieldData; + const firstIK = Array.isArray(fieldData) && fieldData.length > 0 ? fieldData[0] : {}; const { handlePointerDown } = useDraggableGLTF( updatePointToState, diff --git a/app/src/modules/simulation/triggers/triggerHandler/useTriggerHandler.ts b/app/src/modules/simulation/triggers/triggerHandler/useTriggerHandler.ts index b98a527..fbc4dde 100644 --- a/app/src/modules/simulation/triggers/triggerHandler/useTriggerHandler.ts +++ b/app/src/modules/simulation/triggers/triggerHandler/useTriggerHandler.ts @@ -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 } = craneStore(); const { getMachineById, setMachineActive } = machineStore(); const { getStorageUnitById } = storageUnitStore(); const { getMaterialById, setCurrentLocation, setNextLocation, setPreviousLocation, setIsPaused, setIsVisible, setEndTime } = materialStore(); @@ -388,6 +391,65 @@ export function useTriggerHandler() { } } } + } 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 === 'transfer') { + const crane = getCraneById(trigger.triggeredAsset?.triggeredModel.modelUuid); + if (crane) { + setIsPaused(materialId, true); + setCraneScheduled(crane.modelUuid, true); + const conveyor = getConveyorById(action.triggers[0]?.triggeredAsset?.triggeredModel.modelUuid || ''); + if (conveyor) { + addConveyorToMonitor(conveyor.modelUuid, () => { + addCraneToMonitor(crane.modelUuid, () => { + setIsPaused(materialId, true); + + handleAction(action, materialId); + }, action.actionUuid) + }) + } + } + } + } + } + } + } + } + } } } else if (fromEvent?.type === 'vehicle') { if (toEvent?.type === 'transfer') { @@ -1547,6 +1609,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 + } } } diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index 96c529f..62a5759 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -33,7 +33,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(); @@ -56,7 +56,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); diff --git a/app/src/services/dashboard/duplicateProject.ts b/app/src/services/dashboard/duplicateProject.ts index 837bb0e..ca341de 100644 --- a/app/src/services/dashboard/duplicateProject.ts +++ b/app/src/services/dashboard/duplicateProject.ts @@ -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"); + } } }; diff --git a/app/src/services/simulation/ik/getAssetIKs.ts b/app/src/services/factoryBuilder/asset/floorAsset/getAssetField.ts similarity index 80% rename from app/src/services/simulation/ik/getAssetIKs.ts rename to app/src/services/factoryBuilder/asset/floorAsset/getAssetField.ts index cfc6ad5..83567a9 100644 --- a/app/src/services/simulation/ik/getAssetIKs.ts +++ b/app/src/services/factoryBuilder/asset/floorAsset/getAssetField.ts @@ -1,9 +1,9 @@ let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; -export const getAssetIksApi = async (assetId: string) => { +export const getAssetFieldApi = async (assetId: string) => { try { const response = await fetch( - `${url_Backend_dwinzo}/api/v2/getAssetIks/${assetId}`, + `${url_Backend_dwinzo}/api/v2/getAssetField/${assetId}`, { method: "GET", headers: { @@ -20,13 +20,13 @@ export const getAssetIksApi = async (assetId: string) => { } if (!response.ok) { - console.error("Failed to fetch assetIks"); + console.error("Failed to fetch asset field"); } const result = await response.json(); return result; } catch (error) { - echo.error("Failed to get assetIks"); + echo.error("Failed to get asset field"); if (error instanceof Error) { console.log(error.message); } else { diff --git a/app/src/store/builder/useUndoRedo3DStore.ts b/app/src/store/builder/useUndoRedo3DStore.ts new file mode 100644 index 0000000..e8c3ce3 --- /dev/null +++ b/app/src/store/builder/useUndoRedo3DStore.ts @@ -0,0 +1,78 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import { undoRedoConfig } from '../../types/world/worldConstants'; + +type UndoRedo3DStore = { + undoStack: UndoRedo3DTypes[]; + redoStack: UndoRedo3DTypes[]; + + push3D: (entry: UndoRedo3DTypes) => void; + undo3D: () => UndoRedo3DTypes | undefined; + redo3D: () => UndoRedo3DTypes | undefined; + clearUndoRedo3D: () => void; + + peekUndo3D: () => UndoRedo3DTypes | undefined; + peekRedo3D: () => UndoRedo3DTypes | undefined; +}; + +export const createUndoRedo3DStore = () => { + return create()( + immer((set, get) => ({ + undoStack: [], + redoStack: [], + + push3D: (entry) => { + set((state) => { + state.undoStack.push(entry); + + if (state.undoStack.length > undoRedoConfig.undoRedoCount) { + state.undoStack.shift(); + } + + state.redoStack = []; + }); + }, + + undo3D: () => { + let lastAction: UndoRedo3DTypes | undefined; + set((state) => { + lastAction = state.undoStack.pop(); + if (lastAction) { + state.redoStack.unshift(lastAction); + } + }); + return lastAction; + }, + + redo3D: () => { + let redoAction: UndoRedo3DTypes | undefined; + set((state) => { + redoAction = state.redoStack.shift(); + if (redoAction) { + state.undoStack.push(redoAction); + } + }); + return redoAction; + }, + + clearUndoRedo3D: () => { + set((state) => { + state.undoStack = []; + state.redoStack = []; + }); + }, + + peekUndo3D: () => { + const stack = get().undoStack; + return stack.length > 0 ? stack[stack.length - 1] : undefined; + }, + + peekRedo3D: () => { + const stack = get().redoStack; + return stack.length > 0 ? stack[0] : undefined; + }, + })) + ); +} + +export type UndoRedo3DStoreType = ReturnType; \ No newline at end of file diff --git a/app/src/types/builderTypes.d.ts b/app/src/types/builderTypes.d.ts index 63ed97b..3487fc6 100644 --- a/app/src/types/builderTypes.d.ts +++ b/app/src/types/builderTypes.d.ts @@ -253,4 +253,69 @@ type UndoRedo2DTypes = UndoRedo2DDraw | UndoRedo2DUi type UndoRedo2D = { undoStack: UndoRedo2DTypes[]; redoStack: UndoRedo2DTypes[]; +}; + + +// Undo/Redo 3D + +type AssetType = { + type: "Asset"; + assetData: Asset; + newData?: Asset; + eventMetaData?: EventsSchema; + timeStap: string; +} + +type WallAssetType = { + type: "WallAsset"; + assetData: WallAsset; + newData?: WallAsset; + timeStap: string; +} + +type AssetData = AssetType | WallAssetType; + +type UndoRedo3DActionBuilderSchema = { + module: "builder"; + actionType: "Asset-Add" | "Asset-Delete" | "Asset-Update" | "Asset-Duplicated" | "Asset-Copied" | "Wall-Asset-Add" | "Wall-Asset-Delete" | "Wall-Asset-Update"; + asset: AssetData; +} + +type UndoRedo3DActionSimulationSchema = { + module: "simulation"; + actionType: ''; +} + +type UndoRedo3DActionSchema = UndoRedo3DActionBuilderSchema | UndoRedo3DActionSimulationSchema; + +type UndoRedo3DActionsBuilderSchema = { + module: "builder"; + actionType: "Assets-Add" | "Assets-Delete" | "Assets-Update" | "Assets-Duplicated" | "Assets-Copied" | "Wall-Assets-Add" | "Wall-Assets-Delete" | "Wall-Assets-Update"; + assets: AssetData[]; +} + +type UndoRedo3DActionsSimulationSchema = { + module: "simulation"; + actionType: ''; +} + +type UndoRedo3DActionsSchema = UndoRedo3DActionsBuilderSchema | UndoRedo3DActionsSimulationSchema; + +type UndoRedo3DAction = UndoRedo3DActionSchema | UndoRedo3DActionsSchema; + +type UndoRedo3DDraw = { + type: 'Scene'; + actions: UndoRedo3DAction[]; +}; + +type UndoRedo3DUi = { + type: 'UI'; + action: any; // Define UI actions as needed +} + +type UndoRedo3DTypes = UndoRedo3DDraw | UndoRedo3DUi; + +type UndoRedo3D = { + undoStack: UndoRedo3DTypes[]; + redoStack: UndoRedo3DTypes[]; }; \ No newline at end of file diff --git a/app/src/types/simulationTypes.d.ts b/app/src/types/simulationTypes.d.ts index e257ed8..1e90fe8 100644 --- a/app/src/types/simulationTypes.d.ts +++ b/app/src/types/simulationTypes.d.ts @@ -336,6 +336,23 @@ type HumanEventManagerState = { humanStates: HumanEventState[]; }; +type CraneEventState = { + craneId: string; + pendingActions: { + actionUuid: string; + callback: () => void; + }[]; + currentAction: { + actionUuid: string; + callback: () => void; + } | null; + isProcessing: boolean; +}; + +type CraneEventManagerState = { + craneStates: CraneEventState[]; +}; + // Materials diff --git a/app/src/utils/shortcutkeys/handleShortcutKeys.ts b/app/src/utils/shortcutkeys/handleShortcutKeys.ts index f98cd58..f9b8cb8 100644 --- a/app/src/utils/shortcutkeys/handleShortcutKeys.ts +++ b/app/src/utils/shortcutkeys/handleShortcutKeys.ts @@ -191,12 +191,9 @@ const KeyPressListener: React.FC = () => { }; const handleKeyPress = (event: KeyboardEvent) => { - if (isTextInput(document.activeElement)) return; - const keyCombination = detectModifierKeys(event); - if (isTextInput(document.activeElement) && keyCombination !== "ESCAPE") - return; + if (isTextInput(document.activeElement) && keyCombination !== "ESCAPE") return; if (keyCombination === "ESCAPE") { setWalkMode(false);