diff --git a/app/src/app.tsx b/app/src/app.tsx index f5e19b0..0b6fd3e 100644 --- a/app/src/app.tsx +++ b/app/src/app.tsx @@ -3,23 +3,20 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import Dashboard from "./pages/Dashboard"; import Project from "./pages/Project"; import UserAuth from "./pages/UserAuth"; -import ToastProvider from "./components/templates/ToastProvider"; -import "./styles/main.scss" +import "./styles/main.scss"; +import { LoggerProvider } from "./components/ui/log/LoggerContext"; const App: React.FC = () => { return ( - + - } - /> + } /> } /> } /> - + ); }; diff --git a/app/src/components/footer/Footer.tsx b/app/src/components/footer/Footer.tsx new file mode 100644 index 0000000..19fc6c5 --- /dev/null +++ b/app/src/components/footer/Footer.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { HelpIcon } from "../icons/DashboardIcon"; +import { + LogInfoIcon, + ErrorIcon, + WarningIcon, +} from "../icons/ExportCommonIcons"; // Adjust path as needed +import { useLogger } from "../ui/log/LoggerContext"; + +const getLogIcon = (type: string) => { + switch (type) { + case "info": + return ; + case "error": + return ; + case "warning": + return ; + case "log": + default: + return ; + } +}; + +const Footer: React.FC = () => { + const { logs, setIsLogListVisible } = useLogger(); + const lastLog = logs.length > 0 ? logs[logs.length - 1] : null; + + return ( +
+
+
+
+
Selection
+
+
+
+
Rotate/Zoom
+
+
+
+
Pan/Context Menu
+
+
+ +
+
setIsLogListVisible(true)}> + {lastLog ? ( + <> + {getLogIcon(lastLog.type)} + {lastLog.message} + + ) : ( + "No logs yet." + )} +
+
+ V 0.01 +
+ +
+
+
+
+ ); +}; + +export default Footer; diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index eaa7701..4137498 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -458,7 +458,7 @@ export function InfoIcon() { > { ); }; + +export const LogListIcon = () => { + return ( + + + + ); +}; + +export const LogTickIcon = () => { + return ( + + + + + + + + + + + ); +}; + +export const LogInfoIcon = () => { + return ( + + + + + + + ); +}; + +export const WarningIcon = () => { + return ( + + + + + + + ); +}; + +export const ErrorIcon = () => { + return ( + + + + ); +}; diff --git a/app/src/components/icons/SimulationIcons.tsx b/app/src/components/icons/SimulationIcons.tsx index ef23c8f..1e89a51 100644 --- a/app/src/components/icons/SimulationIcons.tsx +++ b/app/src/components/icons/SimulationIcons.tsx @@ -125,21 +125,21 @@ export function ResetIcon() { > @@ -158,7 +158,7 @@ export function PlayStopIcon() { > @@ -177,7 +177,7 @@ export function ExitIcon() { > diff --git a/app/src/components/layout/sidebarLeft/Header.tsx b/app/src/components/layout/sidebarLeft/Header.tsx index 06706c6..0cac495 100644 --- a/app/src/components/layout/sidebarLeft/Header.tsx +++ b/app/src/components/layout/sidebarLeft/Header.tsx @@ -19,7 +19,7 @@ const Header: React.FC = () => { -
{ if (activeModule !== "market") { @@ -29,7 +29,7 @@ const Header: React.FC = () => { }} > -
+ ); }; diff --git a/app/src/components/layout/sidebarRight/analysis/Analysis.tsx b/app/src/components/layout/sidebarRight/analysis/Analysis.tsx index 9d2889f..9b16186 100644 --- a/app/src/components/layout/sidebarRight/analysis/Analysis.tsx +++ b/app/src/components/layout/sidebarRight/analysis/Analysis.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { AI_Icon } from "../../../icons/ExportCommonIcons"; +import { AIIcon } from "../../../icons/ExportCommonIcons"; import RegularDropDown from "../../../ui/inputs/RegularDropDown"; import { AnalysisPresetsType } from "../../../../types/analysis"; import RenderAnalysisInputs from "./RenderAnalysisInputs"; @@ -13,53 +13,24 @@ const Analysis: React.FC = () => { const AnalysisPresets: AnalysisPresetsType = { "Throughput time": [ - { type: "default", inputs: { label: "Height", activeOption: "mm" } }, - { type: "default", inputs: { label: "Width", activeOption: "mm" } }, - { type: "default", inputs: { label: "Length", activeOption: "mtr" } }, - { type: "default", inputs: { label: "Thickness", activeOption: "mm" } }, - { - type: "default", - inputs: { label: "Raw Material", activeOption: "mm" }, - }, - { - type: "range", - inputs: { label: "Material flow", activeOption: "m/min" }, - }, + { type: "default", inputs: { label: "Cycle time", activeOption: "s" } }, + { type: "default", inputs: { label: "machines / lines", activeOption: "item" } }, + { type: "default", inputs: { label: "Machine uptime", activeOption: "%" } }, ], "Production capacity": [ - { type: "default", inputs: { label: "Height", activeOption: "mm" } }, - { type: "default", inputs: { label: "Width", activeOption: "mm" } }, - { type: "default", inputs: { label: "Length", activeOption: "mtr" } }, - { type: "default", inputs: { label: "Thickness", activeOption: "mm" } }, - { - type: "default", - inputs: { label: "Raw Material", activeOption: "mm" }, - }, - { - type: "range", - inputs: { label: "Material flow", activeOption: "m/min" }, - }, - { - type: "range", - inputs: { label: "Shift 1", activeOption: "hr(s)" }, - }, - { - type: "range", - inputs: { label: "Shift 2", activeOption: "hr(s)" }, - }, - { - type: "range", - inputs: { label: "Over time", activeOption: "hr(s)" }, - }, + { type: "default", inputs: { label: "Shift length", activeOption: "hr" } }, + { type: "default", inputs: { label: "Shifts / day", activeOption: "unit" } }, + { type: "default", inputs: { label: "Working days / year", activeOption: "days" } }, + { type: "default", inputs: { label: "Yield rate", activeOption: "%" } }, ], ROI: [ { type: "default", - inputs: { label: "Equipment Cost", activeOption: "INR" }, + inputs: { label: "Selling price", activeOption: "INR" }, }, { type: "default", - inputs: { label: "Employee Salary", activeOption: "INR" }, + inputs: { label: "Material cost", activeOption: "INR" }, }, { type: "default", @@ -67,7 +38,7 @@ const Analysis: React.FC = () => { }, { type: "default", - inputs: { label: "Cost per unit", activeOption: "INR" }, + inputs: { label: "Maintenance cost", activeOption: "INR" }, }, { type: "default", @@ -75,20 +46,19 @@ const Analysis: React.FC = () => { }, { type: "default", - inputs: { label: "Upkeep Cost", activeOption: "INR" }, + inputs: { label: "Fixed costs", activeOption: "INR" }, }, { type: "default", - inputs: { label: "Working Hours", activeOption: "Hrs" }, + inputs: { label: "Salvage value", activeOption: "Hrs" }, }, { type: "default", - inputs: { label: "Power Usage", activeOption: "KWH" }, + inputs: { label: "Production period", activeOption: "yrs" }, }, - { type: "default", inputs: { label: "KWH", activeOption: "Mos" } }, { type: "default", - inputs: { label: "Man Power", activeOption: "Person" }, + inputs: { label: "Tax rate", activeOption: "%" }, }, ], }; @@ -98,7 +68,7 @@ const Analysis: React.FC = () => {
Object
- Generate Report + Generate Report
@@ -121,7 +91,7 @@ const Analysis: React.FC = () => { />
- +
Create Custom Analysis
diff --git a/app/src/components/layout/sidebarRight/properties/GlobalProperties.tsx b/app/src/components/layout/sidebarRight/properties/GlobalProperties.tsx index 8b91afc..514808a 100644 --- a/app/src/components/layout/sidebarRight/properties/GlobalProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/GlobalProperties.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import InputRange from "../../../ui/inputs/InputRange"; import InputToggle from "../../../ui/inputs/InputToggle"; -import { AI_Icon } from "../../../icons/ExportCommonIcons"; +import { AIIcon } from "../../../icons/ExportCommonIcons"; import LabeledButton from "../../../ui/inputs/LabledButton"; import { useAzimuth, @@ -225,7 +225,7 @@ const GlobalProperties: React.FC = () => {
Environment
- + Optimize
diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx index 4fa9105..053fc14 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/EventProperties.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from "react"; import { - useSelectedEventData, - useSelectedEventSphere, - useSelectedProduct, + useSelectedEventData, + useSelectedEventSphere, + useSelectedProduct, } from "../../../../../store/simulation/useSimulationStore"; import { useProductStore } from "../../../../../store/simulation/useProductStore"; import ConveyorMechanics from "./mechanics/conveyorMechanics"; @@ -15,114 +15,118 @@ import { handleAddEventToProduct } from "../../../../../modules/simulation/event import { useEventsStore } from "../../../../../store/simulation/useEventsStore"; const EventProperties: React.FC = () => { - const { selectedEventData } = useSelectedEventData(); - const { getEventByModelUuid } = useProductStore(); - const { selectedProduct } = useSelectedProduct(); - const [currentEventData, setCurrentEventData] = useState(null); - const [assetType, setAssetType] = useState(null); - const { products, addEvent } = useProductStore(); - const { selectedEventSphere } = useSelectedEventSphere(); + const { selectedEventData } = useSelectedEventData(); + const { getEventByModelUuid } = useProductStore(); + const { selectedProduct } = useSelectedProduct(); + const [currentEventData, setCurrentEventData] = useState( + null + ); + const [assetType, setAssetType] = useState(null); + const { products, addEvent } = useProductStore(); + const { selectedEventSphere } = useSelectedEventSphere(); - useEffect(() => { - const event = getCurrentEventData(); - setCurrentEventData(event); + useEffect(() => { + const event = getCurrentEventData(); + setCurrentEventData(event); - const type = determineAssetType(event); - setAssetType(type); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedEventData, selectedProduct]); - - const getCurrentEventData = () => { - if (!selectedEventData?.data || !selectedProduct) return null; - return ( - getEventByModelUuid( - selectedProduct.productId, - selectedEventData.data.modelUuid - ) ?? null - ); - }; - - const determineAssetType = (event: EventsSchema | null) => { - if (!event) return null; - - switch (event.type) { - case "transfer": - return "conveyor"; - case "vehicle": - return "vehicle"; - case "roboticArm": - return "roboticArm"; - case "machine": - return "machine"; - case "storageUnit": - return "storageUnit"; - default: - return null; - } - }; + const type = determineAssetType(event); + setAssetType(type); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedEventData, selectedProduct]); + const getCurrentEventData = () => { + if (!selectedEventData?.data || !selectedProduct) return null; return ( -
- {currentEventData && ( - <> -
-
- {selectedEventData?.data.modelName} -
-
- {assetType === "conveyor" && } - {assetType === "vehicle" && } - {assetType === "roboticArm" && } - {assetType === "machine" && } - {assetType === "storageUnit" && } - - )} - {!currentEventData && selectedEventSphere && ( -
-

- Oops! It looks like this object doesn't have an - event assigned yet. To continue, please link it to one of the - products below. -

- -
-

- Here are some products you can add it to: -

-
    - {products.map((product) => ( -
  • - -
  • - ))} -
-
-
- ) - } - {!selectedEventSphere && ( -
-

- Oops! It looks like you haven't selected an event - point yet. Please select an event to view its properties. -

-
- )} -
+ getEventByModelUuid( + selectedProduct.productId, + selectedEventData.data.modelUuid + ) ?? null ); + }; + + const determineAssetType = (event: EventsSchema | null) => { + if (!event) return null; + + switch (event.type) { + case "transfer": + return "conveyor"; + case "vehicle": + return "vehicle"; + case "roboticArm": + return "roboticArm"; + case "machine": + return "machine"; + case "storageUnit": + return "storageUnit"; + default: + return null; + } + }; + + return ( +
+ {currentEventData && ( + <> +
+
+ {selectedEventData?.data.modelName} +
+
+ {assetType === "conveyor" && } + {assetType === "vehicle" && } + {assetType === "roboticArm" && } + {assetType === "machine" && } + {assetType === "storageUnit" && } + + )} + {!currentEventData && selectedEventSphere && ( +
+

+ Oops! It looks like this object doesn't have an + event assigned yet. To continue, please link it to one of the + products below. +

+ +
+

+ Here are some products you can add it to: +

+
+ {products.map((product) => ( + + ))} +
+
+
+ )} + {!selectedEventSphere && ( +
+

+ Oops! It looks like you haven't selected an event + point yet. Please select an event to view its properties. +

+
+ )} +
+ ); }; export default EventProperties; diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/components/ActionsList.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/components/ActionsList.tsx index 09017de..8aae3b2 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/components/ActionsList.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/components/ActionsList.tsx @@ -35,7 +35,7 @@ const ActionsList: React.FC = ({ const handleRenameAction = (newName: string) => { if (!selectedAction.actionId) return; - const event = renameAction(selectedAction.actionId, newName); + const event = renameAction(selectedProduct.productId, selectedAction.actionId, newName); if (event) { upsertProductOrEventApi({ diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx index f68b6b8..a6a1fa9 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/conveyorMechanics.tsx @@ -72,7 +72,7 @@ function ConveyorMechanics() { const validOption = option as | "default" | "spawn" | "swap" | "delay" | "despawn"; setActiveOption(validOption); - const event = updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionType: validOption, }); @@ -88,7 +88,7 @@ function ConveyorMechanics() { const handleRenameAction = (newName: string) => { if (!selectedEventData || !selectedPointData) return; - const event = updateAction(selectedPointData.action.actionUuid, { actionName: newName }); + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionName: newName }); if (event) { updateBackend( @@ -102,7 +102,7 @@ function ConveyorMechanics() { const handleSpawnCountChange = (value: string) => { if (!selectedEventData || !selectedPointData) return; - const event = updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { spawnCount: value === "inherit" ? "inherit" : parseFloat(value), }); @@ -118,7 +118,7 @@ function ConveyorMechanics() { const handleSpawnIntervalChange = (value: string) => { if (!selectedEventData || !selectedPointData) return; - const event = updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { spawnInterval: value === "inherit" ? "inherit" : parseFloat(value), }); @@ -134,7 +134,7 @@ function ConveyorMechanics() { const handleMaterialSelect = (material: string) => { if (!selectedEventData || !selectedPointData) return; - const event = updateAction(selectedPointData.action.actionUuid, { material }); + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { material }); if (event) { updateBackend( @@ -148,7 +148,7 @@ function ConveyorMechanics() { const handleDelayChange = (value: string) => { if (!selectedEventData || !selectedPointData) return; - const event = updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { delay: value === "inherit" ? "inherit" : parseFloat(value), }); @@ -271,7 +271,7 @@ function ConveyorMechanics() {
- +
diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/machineMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/machineMechanics.tsx index 993c46b..be50990 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/machineMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/machineMechanics.tsx @@ -6,6 +6,7 @@ import { useSelectedEventData, useSelectedProduct } from "../../../../../../stor import { useProductStore } from "../../../../../../store/simulation/useProductStore"; import ProcessAction from "../actions/ProcessAction"; import ActionsList from "../components/ActionsList"; +import { upsertProductOrEventApi } from "../../../../../../services/simulation/UpsertProductOrEventApi"; function MachineMechanics() { const [activeOption, setActiveOption] = useState<"default" | "process">("default"); @@ -14,6 +15,9 @@ function MachineMechanics() { const { getPointByUuid, updateAction } = useProductStore(); const { selectedProduct } = useSelectedProduct(); + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + useEffect(() => { if (selectedEventData) { const point = getPointByUuid( @@ -28,31 +32,54 @@ function MachineMechanics() { } }, [selectedProduct, selectedEventData, getPointByUuid]); + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData + }) + } + const handleActionTypeChange = (option: string) => { if (!selectedEventData || !selectedPointData) return; const validOption = option as "process"; setActiveOption(validOption); - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionType: validOption, }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handleRenameAction = (newName: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { actionName: newName }); + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionName: newName }); }; const handleProcessTimeChange = (value: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { processTime: parseFloat(value), }); }; const handleMaterialSelect = (material: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { swapMaterial: material, }); }; @@ -110,7 +137,7 @@ function MachineMechanics() {
- +
)} diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx index e56741f..3e8aa67 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/roboticArmMechanics.tsx @@ -59,7 +59,7 @@ function RoboticArmMechanics() { const handleRenameAction = (newName: string) => { if (!selectedAction.actionId) return; - const event = updateAction(selectedAction.actionId, { actionName: newName }); + const event = updateAction(selectedProduct.productId, selectedAction.actionId, { actionName: newName }); if (selectedPointData) { const updatedActions = selectedPointData.actions.map((action) => @@ -101,7 +101,7 @@ function RoboticArmMechanics() { if (!selectedAction.actionId || !selectedPointData) return; const [x, y, z] = value.split(",").map(Number); - const event = updateAction(selectedAction.actionId, { + const event = updateAction(selectedProduct.productId, selectedAction.actionId, { process: { startPoint: [x, y, z] as [number, number, number], endPoint: selectedPointData.actions.find((a) => a.actionUuid === selectedAction.actionId)?.process.endPoint || null, @@ -122,7 +122,7 @@ function RoboticArmMechanics() { if (!selectedAction.actionId || !selectedPointData) return; const [x, y, z] = value.split(",").map(Number); - const event = updateAction(selectedAction.actionId, { + const event = updateAction(selectedProduct.productId, selectedAction.actionId, { process: { startPoint: selectedPointData.actions.find((a) => a.actionUuid === selectedAction.actionId)?.process.startPoint || null, endPoint: [x, y, z] as [number, number, number], @@ -181,7 +181,7 @@ function RoboticArmMechanics() { const handleDeleteAction = (actionUuid: string) => { if (!selectedPointData) return; - const event = removeAction(actionUuid); + const event = removeAction(selectedProduct.productId, actionUuid); if (event) { updateBackend( @@ -280,7 +280,7 @@ function RoboticArmMechanics() { />
- +
)} diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx index c85da9f..34e35ae 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/storageMechanics.tsx @@ -6,6 +6,7 @@ import { useSelectedEventData, useSelectedProduct } from "../../../../../../stor import { useProductStore } from "../../../../../../store/simulation/useProductStore"; import StorageAction from "../actions/StorageAction"; import ActionsList from "../components/ActionsList"; +import { upsertProductOrEventApi } from "../../../../../../services/simulation/UpsertProductOrEventApi"; function StorageMechanics() { const [activeOption, setActiveOption] = useState<"default" | "store" | "spawn">("default"); @@ -14,6 +15,9 @@ function StorageMechanics() { const { getPointByUuid, updateAction } = useProductStore(); const { selectedProduct } = useSelectedProduct(); + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + useEffect(() => { if (selectedEventData) { const point = getPointByUuid( @@ -28,26 +32,67 @@ function StorageMechanics() { } }, [selectedProduct, selectedEventData, getPointByUuid]); + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData + }) + } + const handleActionTypeChange = (option: string) => { if (!selectedEventData || !selectedPointData) return; const validOption = option as "store" | "spawn"; setActiveOption(validOption); - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionType: validOption, }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handleRenameAction = (newName: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { actionName: newName }); + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionName: newName }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handleCapacityChange = (value: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { storageCapacity: parseInt(value), }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; // Get current values from store @@ -101,7 +146,7 @@ function StorageMechanics() {
- +
)} diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx index 28eb61f..0f52437 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/mechanics/vehicleMechanics.tsx @@ -10,6 +10,7 @@ import { import { useProductStore } from "../../../../../../store/simulation/useProductStore"; import TravelAction from "../actions/TravelAction"; import ActionsList from "../components/ActionsList"; +import { upsertProductOrEventApi } from "../../../../../../services/simulation/UpsertProductOrEventApi"; function VehicleMechanics() { const [activeOption, setActiveOption] = useState<"default" | "travel">("default"); @@ -18,8 +19,11 @@ function VehicleMechanics() { const { getPointByUuid, getEventByModelUuid, updateEvent, updateAction } = useProductStore(); const { selectedProduct } = useSelectedProduct(); + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + useEffect(() => { - if (selectedEventData) { + if (selectedEventData && selectedEventData.data.type === 'vehicle') { const point = getPointByUuid( selectedProduct.productId, selectedEventData.data.modelUuid, @@ -33,11 +37,34 @@ function VehicleMechanics() { } }, [selectedProduct, selectedEventData, getPointByUuid]); + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData + }) + } + const handleSpeedChange = (value: string) => { if (!selectedEventData) return; - updateEvent(selectedProduct.productId, selectedEventData.data.modelUuid, { + const event = updateEvent(selectedProduct.productId, selectedEventData.data.modelUuid, { speed: parseFloat(value), }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handleActionTypeChange = (option: string) => { @@ -45,28 +72,64 @@ function VehicleMechanics() { const validOption = option as "travel"; setActiveOption(validOption); - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionType: validOption, }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handleRenameAction = (newName: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { actionName: newName }); + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { actionName: newName }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handleLoadCapacityChange = (value: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { loadCapacity: parseFloat(value), }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handleUnloadDurationChange = (value: string) => { if (!selectedPointData) return; - updateAction(selectedPointData.action.actionUuid, { + const event = updateAction(selectedProduct.productId, selectedPointData.action.actionUuid, { unLoadDuration: parseFloat(value), }); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } }; const handlePickPointChange = (value: string) => { @@ -172,7 +235,7 @@ function VehicleMechanics() {
- +
diff --git a/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx b/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx index 1434446..f48f875 100644 --- a/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx +++ b/app/src/components/layout/sidebarRight/properties/eventProperties/trigger/Trigger.tsx @@ -1,137 +1,325 @@ -import React, { useRef, useState } from "react"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import * as THREE from "three"; import { - AddIcon, - RemoveIcon, - ResizeHeightIcon, + AddIcon, + RemoveIcon, + ResizeHeightIcon, } from "../../../../../icons/ExportCommonIcons"; import LabledDropdown from "../../../../../ui/inputs/LabledDropdown"; import RenameInput from "../../../../../ui/inputs/RenameInput"; import { handleResize } from "../../../../../../functions/handleResizePannel"; +import { useProductStore } from "../../../../../../store/simulation/useProductStore"; +import { useSelectedProduct } from "../../../../../../store/simulation/useSimulationStore"; +import { upsertProductOrEventApi } from "../../../../../../services/simulation/UpsertProductOrEventApi"; -const Trigger: React.FC = () => { - // State to hold the list of triggers - const [triggers, setTriggers] = useState(["Trigger 1"]); - const [selectedTrigger, setSelectedTrigger] = useState("Trigger 1"); - const [activeOption, setActiveOption] = useState("onComplete"); - const triggersContainerRef = useRef(null); +type TriggerProps = { + selectedPointData?: PointsScheme | undefined; + type?: 'Conveyor' | 'Vehicle' | 'RoboticArm' | 'Machine' | 'StorageUnit'; +}; - // States for dropdowns - const [triggeredModel, setTriggeredModel] = useState([]); - const [triggeredPoint, setTriggeredPoint] = useState([]); - const [triggeredAction, setTriggeredAction] = useState([]); +const Trigger = ({ selectedPointData, type }: TriggerProps) => { + const [currentAction, setCurrentAction] = useState(); + const { selectedProduct } = useSelectedProduct(); + const { getActionByUuid, addTrigger, removeTrigger, updateTrigger, renameTrigger, getProductById } = useProductStore(); + const [triggers, setTriggers] = useState([]); + const [selectedTrigger, setSelectedTrigger] = useState(); + const [activeOption, setActiveOption] = useState<"onComplete" | "onStart" | "onStop" | "delay" | "onError">("onComplete"); + const triggersContainerRef = useRef(null); - // Function to handle adding a new trigger - const addTrigger = (): void => { - const newTrigger = `Trigger ${triggers.length + 1}`; - setTriggers([...triggers, newTrigger]); // Add new trigger to the state + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; - // Initialize the states for the new trigger - setTriggeredModel([...triggeredModel, ""]); - setTriggeredPoint([...triggeredPoint, ""]); - setTriggeredAction([...triggeredAction, ""]); - }; + useEffect(() => { + if (!selectedPointData || !selectedProduct) return; - // Function to handle removing a trigger - const removeTrigger = (index: number): void => { - setTriggers(triggers.filter((_, i) => i !== index)); // Remove trigger by index - setTriggeredModel(triggeredModel.filter((_, i) => i !== index)); - setTriggeredPoint(triggeredPoint.filter((_, i) => i !== index)); - setTriggeredAction(triggeredAction.filter((_, i) => i !== index)); - }; + let actionUuid: string | undefined; - return ( -
-
-
Trigger
- -
-
-
-
- {triggers.map((trigger: any, index: number) => ( -
setSelectedTrigger(trigger)} - > - - {triggers.length > 1 && ( - +
+
+
+
+ {triggers.map((trigger) => ( +
setSelectedTrigger(trigger)} + > + + {triggers.length > 1 && ( + + )} +
+ ))} +
+ +
+ + {selectedTrigger && ( +
+
{selectedTrigger.triggerName}
+ + +
+ (option.modelName))]} + onSelect={(option) => { handleModelSelect(option, selectedTrigger.triggerUuid) }} + /> + (`Point ${option.uuid.slice(0, 5)}`))]} + onSelect={(option) => { handlePointSelect(option, selectedTrigger.triggerUuid) }} + /> + (option.actionName))]} + onSelect={(option) => { handleActionSelect(option, selectedTrigger.triggerUuid) }} + /> +
+
)} -
- ))} -
- +
-
-
{selectedTrigger}
- setActiveOption(option)} - /> -
- { - const newModel = [...triggeredModel]; - newModel[0] = option; - setTriggeredModel(newModel); - }} - /> - { - const newPoint = [...triggeredPoint]; - newPoint[0] = option; - setTriggeredPoint(newPoint); - }} - /> - { - const newAction = [...triggeredAction]; - newAction[0] = option; - setTriggeredAction(newAction); - }} - /> -
-
-
- - ); + ); }; export default Trigger; diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index 21ea6a7..14bc517 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -36,7 +36,6 @@ import { import useToggleStore from "../../store/useUIToggleStore"; import { use3DWidget, - useDroppedObjectsStore, useFloatingWidget, } from "../../store/visualization/useDroppedObjectsStore"; @@ -57,20 +56,18 @@ const Tools: React.FC = () => { const { widgets3D } = use3DWidget(); - const zones = useDroppedObjectsStore((state) => state.zones); - // wall options const { toggleView, setToggleView } = useToggleView(); const { setDeleteTool } = useDeleteTool(); const { setAddAction } = useAddAction(); const { setSelectedWallItem } = useSelectedWallItem(); - const { transformMode, setTransformMode } = useTransformMode(); - const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); - const { movePoint, setMovePoint } = useMovePoint(); - const { toolMode, setToolMode } = useToolMode(); + const { setTransformMode } = useTransformMode(); + const { setDeletePointOrLine } = useDeletePointOrLine(); + const { setMovePoint } = useMovePoint(); + const { setToolMode } = useToolMode(); const { activeTool, setActiveTool } = useActiveTool(); - const { refTextupdate, setRefTextUpdate } = useRefTextUpdate(); + const { setRefTextUpdate } = useRefTextUpdate(); // Reset activeTool whenever activeModule changes useEffect(() => { @@ -209,268 +206,266 @@ const Tools: React.FC = () => { return ( <> {!isPlaying ? ( - <> -
-
-
- {activeSubTool == "cursor" && ( -
{ - setActiveTool("cursor"); - }} - > - -
- )} - {activeSubTool == "free-hand" && ( -
{ - setActiveTool("free-hand"); - }} - > - -
- )} - {activeSubTool == "delete" && ( -
{ - setActiveTool("delete"); - }} - > - -
- )} - {activeModule !== "visualization" && ( -
{ - setOpenDrop(!openDrop); - }} - > - - {openDrop && ( -
-
{ - setOpenDrop(false); - setActiveTool("cursor"); - setActiveSubTool("cursor"); - }} - > -
- {activeSubTool === "cursor" && } -
- -
Cursor
-
-
{ - setOpenDrop(false); - setActiveTool("free-hand"); - setActiveSubTool("free-hand"); - }} - > -
- {activeSubTool === "free-hand" && } -
- -
Free Hand
-
-
{ - setOpenDrop(false); - setActiveTool("delete"); - setActiveSubTool("delete"); - }} - > -
- {activeSubTool === "delete" && } -
- -
Delete
-
-
- )} -
- )} -
-
- {!toggleThreeD && activeModule === "builder" && ( - <> -
-
-
{ - setActiveTool("draw-wall"); - }} - title="Wall" - > - -
-
{ - setActiveTool("draw-zone"); - }} - title="Zone" - > - -
-
{ - setActiveTool("draw-aisle"); - }} - title="Aisle" - > - -
-
{ - setActiveTool("draw-floor"); - }} - title="Floor" - > - -
-
- - )} - {activeModule === "builder" && ( - <> -
-
-
{ - setActiveTool("measure"); - }} - title="Measure" - > - -
-
- - )} - {activeModule === "simulation" && ( - <> -
-
-
{ - setActiveTool("pen"); - }} - > - -
-
- - )} - {activeModule === "visualization" && ( - <> -
-
-
{ - handleSaveTemplate({ - addTemplate, - floatingWidget, - widgets3D, - selectedZone, - templates, - visualizationSocket, - }); - }} - > - -
-
- - )} -
-
-
{ - setActiveTool("comment"); - }} - > - -
- {toggleThreeD && ( +
+
+
+ {activeSubTool == "cursor" && (
{ - setIsPlaying(!isPlaying); + setActiveTool("cursor"); }} > - + +
+ )} + {activeSubTool == "free-hand" && ( +
{ + setActiveTool("free-hand"); + }} + > + +
+ )} + {activeSubTool == "delete" && ( +
{ + setActiveTool("delete"); + }} + > + +
+ )} + {activeModule !== "visualization" && ( +
{ + setOpenDrop(!openDrop); + }} + > + + {openDrop && ( +
+
{ + setOpenDrop(false); + setActiveTool("cursor"); + setActiveSubTool("cursor"); + }} + > +
+ {activeSubTool === "cursor" && } +
+ +
Cursor
+
+
{ + setOpenDrop(false); + setActiveTool("free-hand"); + setActiveSubTool("free-hand"); + }} + > +
+ {activeSubTool === "free-hand" && } +
+ +
Free Hand
+
+
{ + setOpenDrop(false); + setActiveTool("delete"); + setActiveSubTool("delete"); + }} + > +
+ {activeSubTool === "delete" && } +
+ +
Delete
+
+
+ )}
)}
- {activeModule === "builder" && ( - <> -
+
+ {!toggleThreeD && activeModule === "builder" && ( + <> +
+
{ + setActiveTool("draw-wall"); + }} + title="Wall" > -
- 2d -
-
- 3d -
+
- +
{ + setActiveTool("draw-zone"); + }} + title="Zone" + > + +
+
{ + setActiveTool("draw-aisle"); + }} + title="Aisle" + > + +
+
{ + setActiveTool("draw-floor"); + }} + title="Floor" + > + +
+
+ + )} + {activeModule === "builder" && ( + <> +
+
+
{ + setActiveTool("measure"); + }} + title="Measure" + > + +
+
+ + )} + {activeModule === "simulation" && ( + <> +
+
+
{ + setActiveTool("pen"); + }} + > + +
+
+ + )} + {activeModule === "visualization" && ( + <> +
+
+
{ + handleSaveTemplate({ + addTemplate, + floatingWidget, + widgets3D, + selectedZone, + templates, + visualizationSocket, + }); + }} + > + +
+
+ + )} +
+
+
{ + setActiveTool("comment"); + }} + > + +
+ {toggleThreeD && ( +
{ + setIsPlaying(!isPlaying); + }} + > + +
)}
- + {activeModule === "builder" && ( + <> +
+
+
+ 2d +
+
+ 3d +
+
+ + )} +
) : ( <> {activeModule !== "simulation" && ( -
setIsPlaying(false)}> +
+ )} )} diff --git a/app/src/components/ui/analysis/ROISummary.tsx b/app/src/components/ui/analysis/ROISummary.tsx index 11101fd..63ae707 100644 --- a/app/src/components/ui/analysis/ROISummary.tsx +++ b/app/src/components/ui/analysis/ROISummary.tsx @@ -1,6 +1,7 @@ import React, { useState } from "react"; import { ROISummaryIcon } from "../../icons/analysis"; import SemiCircleProgress from "./SemiCircleProgress"; +import { ArrowIcon } from "../../icons/ExportCommonIcons"; const ROISummary = ({ roiSummaryData = { @@ -121,7 +122,7 @@ const ROISummary = ({
- {isTableOpen ? "⌵" : "⌵"} +
{ + const { logs, clear, setIsLogListVisible } = useLogger(); + const [selectedTab, setSelectedTab] = useState< + "all" | "info" | "warning" | "error" | "log" + >("all"); + + const getLogIcon = (type: string) => { + switch (type) { + case "info": + return ; + case "error": + return ; + case "warning": + return ; + case "log": + default: + return ; + } + }; + + const formatTimestamp = (date: Date) => new Date(date).toLocaleTimeString(); + + const filteredLogs = + selectedTab === "all" + ? [...logs].reverse() + : [...logs].filter((log) => log.type === selectedTab).reverse(); + + return ( + // eslint-disable-next-line +
setIsLogListVisible(false)} + > +
{ + e.stopPropagation(); + }} + > +
+
+
+ +
+
Log List
+
+ +
+ + {/* Tabs */} +
+ {["all", "info", "warning", "error"].map((type) => ( +
setSelectedTab(type as any)} + > + {`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`} +
+ ))} +
+ + {/* Log Entries */} +
+ {filteredLogs.map((log) => ( +
+
{getLogIcon(log.type)}
+
+
{log.message}
+
+ {formatTimestamp(log.timestamp)} +
+
+
+ ))} +
+
+
+ ); +}; + +export default LogList; diff --git a/app/src/components/ui/log/LoggerContext.tsx b/app/src/components/ui/log/LoggerContext.tsx new file mode 100644 index 0000000..f323b93 --- /dev/null +++ b/app/src/components/ui/log/LoggerContext.tsx @@ -0,0 +1,77 @@ +import React, { createContext, useContext, useState, useCallback } from "react"; + +export type LogType = "log" | "info" | "warning" | "error"; + +export interface LogEntry { + id: string; + type: LogType; + message: string; + timestamp: Date; +} + +interface LoggerContextValue { + logs: LogEntry[]; + setLogs: React.Dispatch>; + isLogListVisible: boolean; + setIsLogListVisible: React.Dispatch>; + log: (message: string) => void; + info: (message: string) => void; + warn: (message: string) => void; + error: (message: string) => void; + clear: () => void; +} + +const LoggerContext = createContext(undefined); + +export const LoggerProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [logs, setLogs] = useState([]); + const [isLogListVisible, setIsLogListVisible] = useState(false); + + const generateId = useCallback( + () => Math.random().toString(36).substring(2, 9), + [] + ); + + const addLog = useCallback( + (type: LogType, message: string) => { + const newLog: LogEntry = { + id: generateId(), + type, + message, + timestamp: new Date(), + }; + setLogs((prevLogs) => [...prevLogs, newLog]); + }, + [generateId] + ); + + const loggerMethods: LoggerContextValue = { + logs, + setLogs, + isLogListVisible, + setIsLogListVisible, + log: (message: string) => addLog("log", message), + info: (message: string) => addLog("info", message), + warn: (message: string) => addLog("warning", message), + error: (message: string) => addLog("error", message), + clear: () => { + setLogs([]); + }, + }; + + return ( + + {children} + + ); +}; + +export const useLogger = () => { + const context = useContext(LoggerContext); + if (!context) { + throw new Error("useLogger must be used within a LoggerProvider"); + } + return context; +}; diff --git a/app/src/components/ui/log/logger.ts b/app/src/components/ui/log/logger.ts new file mode 100644 index 0000000..1781e26 --- /dev/null +++ b/app/src/components/ui/log/logger.ts @@ -0,0 +1,71 @@ +type LogType = 'log' | 'info' | 'warning' | 'error'; + +interface LogEntry { + type: LogType; + message: string; + timestamp: Date; + context?: string; +} + +class Logger { + private static instance: Logger; + private logs: LogEntry[] = []; + private subscribers: Array<(log: LogEntry) => void> = []; + + private constructor() {} + + public static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + private notifySubscribers(log: LogEntry) { + this.subscribers.forEach(callback => callback(log)); + } + + private addLog(type: LogType, message: string, context?: string) { + const log: LogEntry = { type, message, timestamp: new Date(), context }; + this.logs.push(log); + this.notifySubscribers(log); + + if (process.env.NODE_ENV === 'development') { + const logMessage = context ? `[${context}] ${message}` : message; + console[type === 'warning' ? 'warn' : type](logMessage); + } + } + + public log(message: string, context?: string) { + this.addLog('log', message, context); + } + + public info(message: string, context?: string) { + this.addLog('info', message, context); + } + + public warning(message: string, context?: string) { + this.addLog('warning', message, context); + } + + public error(message: string, context?: string) { + this.addLog('error', message, context); + } + + public getLogs(): LogEntry[] { + return [...this.logs]; + } + + public clear() { + this.logs = []; + } + + public subscribe(callback: (log: LogEntry) => void) { + this.subscribers.push(callback); + return () => { + this.subscribers = this.subscribers.filter(sub => sub !== callback); + }; + } +} + +export const logger = Logger.getInstance(); diff --git a/app/src/components/ui/simulation/simulationPlayer.tsx b/app/src/components/ui/simulation/simulationPlayer.tsx index 4c7c76d..e569250 100644 --- a/app/src/components/ui/simulation/simulationPlayer.tsx +++ b/app/src/components/ui/simulation/simulationPlayer.tsx @@ -12,25 +12,31 @@ import { EndIcon, ExpandIcon, HourlySimulationIcon, + InfoIcon, MonthlyROI, SpeedIcon, StartIcon, } from "../../icons/ExportCommonIcons"; import { getAvatarColor } from "../../../modules/collaboration/functions/getAvatarColor"; +import { useSubModuleStore } from "../../../store/useModuleStore"; +import ProductionCapacity from "../analysis/ProductionCapacity"; +import ThroughputSummary from "../analysis/ThroughputSummary"; +import ROISummary from "../analysis/ROISummary"; const SimulationPlayer: React.FC = () => { const MAX_SPEED = 8; // Maximum speed + const isDragging = useRef(false); + const sliderRef = useRef(null); const [expand, setExpand] = useState(true); + const [playSimulation, setPlaySimulation] = useState(false); const { speed, setSpeed } = useAnimationPlaySpeed(); - const [playSimulation, setPlaySimulation] = useState(false); const { setIsPlaying } = usePlayButtonStore(); - const sliderRef = useRef(null); - const isDragging = useRef(false); const { setActiveTool } = useActiveTool(); const { isPaused, setIsPaused } = usePauseButtonStore(); const { isReset, setReset } = useResetButtonStore(); + const { subModule } = useSubModuleStore(); // Button functions const handleReset = () => { @@ -114,7 +120,7 @@ const SimulationPlayer: React.FC = () => { }; // Store colors for each process item - const [processColors, setProcessColors] = useState([]); + const [_, setProcessColors] = useState([]); // Generate colors on mount or when process changes useEffect(() => { @@ -159,210 +165,236 @@ const SimulationPlayer: React.FC = () => { }; return ( -
-
-
-
- {/* hourlySimulation */} -
-
-
- + <> +
+
+
+ {subModule === "analysis" && ( +
+ {/* hourlySimulation */} +
+
+
+ +
+
Hourly Simulation
+
+
+
+
-
Hourly Simulation
-
-
-
-
-
- {/* dailyProduction */} -
-
-
- + {/* dailyProduction */} +
+
+
+ +
+
Daily Production
+
+
+
+
-
Daily Production
-
-
-
-
-
- {/* monthlyROI */} -
-
-
- + {/* monthlyROI */} +
+
+
+ +
+
Monthly ROI
+
+
+
+
{" "}
-
Monthly ROI
-
-
-
{" "} -
-
-
- - - - -
-
-
-
-
-
- + )} + {subModule === "simulations" && ( +
+ + {playSimulation + ? "Paused - system idle." + : "Running simulation..."}
-
-
23 April ,25
-
04:41 PM
-
-
-
-
- {intervals.map((label, index) => { - const segmentProgress = (index / totalSegments) * 100; - const isFilled = progress >= segmentProgress; - return ( - -
-
{label} mins
-
-
- {index < intervals.length - 1 && ( -
= ((index + 1) / totalSegments) * 100 - ? "filled" - : "" - }`} - >
- )} -
- ); - })} -
-
- -
-
-
00:10:20
-
-
- -
-
-
-
-
-
- -
- Speed -
-
-
0.5X
-
-
-
-
-
-
-
-
-
-
+ )} +
+ + + + {subModule === "analysis" && ( - -
-
4x
+ )}
-
-
-
00:00
-
24:00
-
-
- {process.map((item, index) => ( -
-
+
+
+
+
+
- ))} +
+
23 April ,25
+
04:41 PM
+
+
+
+
+ {intervals.map((label, index) => { + const segmentProgress = (index / totalSegments) * 100; + const isFilled = progress >= segmentProgress; + return ( + +
+
{label} mins
+
+
+ {index < intervals.length - 1 && ( +
= ((index + 1) / totalSegments) * 100 + ? "filled" + : "" + }`} + >
+ )} +
+ ); + })} +
+
+ +
+
+
00:10:20
+
+
+ +
+
+
+
+
+
+ +
+ Speed +
+
+
0.5X
+
+
+
+
+
+
+
+
+
+
+ + +
+
4x
+
+ {subModule === "analysis" && ( +
+
00:00
+
24:00
+
+
+ {process.map((item, index) => ( +
+
+
+ ))} +
+
+
+ )}
-
+
+
+ + +
+ +
+ ); }; diff --git a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts index a351d73..eab6e3e 100644 --- a/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts +++ b/app/src/modules/builder/IntialLoad/loadInitialFloorItems.ts @@ -8,7 +8,6 @@ import * as Types from "../../../types/world/worldTypes"; import { initializeDB, retrieveGLTF, storeGLTF } from '../../../utils/indexDB/idbUtils'; import { getCamera } from '../../../services/factoryBuilder/camera/getCameraApi'; import { getFloorAssets } from '../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi'; -import PointsCalculator from '../../simulation/events/points/functions/pointsCalculator'; async function loadInitialFloorItems( itemsGroup: Types.RefGroup, @@ -72,7 +71,7 @@ async function loadInitialFloorItems( // Check Three.js Cache const cachedModel = THREE.Cache.get(item.modelfileID!); if (cachedModel) { - // console.log(`[Cache] Fetching ${item.modelName}`); + // processLoadedModel(cachedModel.scene.clone(), item, itemsGroup, setFloorItems, addEvent); modelsLoaded++; checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve); @@ -82,7 +81,7 @@ async function loadInitialFloorItems( // Check IndexedDB const indexedDBModel = await retrieveGLTF(item.modelfileID!); if (indexedDBModel) { - // console.log(`[IndexedDB] Fetching ${item.modelName}`); + // const blobUrl = URL.createObjectURL(indexedDBModel); loader.load(blobUrl, (gltf) => { URL.revokeObjectURL(blobUrl); @@ -103,7 +102,7 @@ async function loadInitialFloorItems( } // Fetch from Backend - // console.log(`[Backend] Fetching ${item.modelName}`); + // const modelUrl = `${url_Backend_dwinzo}/api/v2/AssetFile/${item.modelfileID!}`; loader.load(modelUrl, async (gltf) => { const modelBlob = await fetch(modelUrl).then((res) => res.blob()); @@ -121,7 +120,7 @@ async function loadInitialFloorItems( ); }); } else { - // console.log(`Item ${item.modelName} is not near`); + // setFloorItems((prevItems) => [ ...(prevItems || []), { @@ -187,7 +186,7 @@ function processLoadedModel( }, ]); - if (item.eventData.type === "vehicle") { + if (item.eventData.type === "Vehicle") { const vehicleEvent: VehicleEventSchema = { modelUuid: item.modelUuid, modelName: item.modelName, @@ -202,11 +201,11 @@ function processLoadedModel( rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0], action: { actionUuid: THREE.MathUtils.generateUUID(), - actionName: "Vehicle Action", + actionName: "Action 1", actionType: "travel", unLoadDuration: 5, loadCapacity: 10, - steeringAngle:0, + steeringAngle: 0, pickUpPoint: null, unLoadPoint: null, triggers: [] @@ -254,7 +253,7 @@ function processLoadedModel( rotation: [item.eventData.point?.rotation[0] || 0, item.eventData.point?.rotation[1] || 0, item.eventData.point?.rotation[2] || 0], action: { actionUuid: THREE.MathUtils.generateUUID(), - actionName: "Process Action", + actionName: "Action 1", actionType: "process", processTime: 10, swapMaterial: "material-id", @@ -279,11 +278,11 @@ function processLoadedModel( actions: [ { actionUuid: THREE.MathUtils.generateUUID(), - actionName: "Pick and Place", + actionName: "Action 1", actionType: "pickAndPlace", process: { - startPoint: [0, 0, 0], - endPoint: [0, 0, 0] + startPoint: null, + endPoint: null }, triggers: [] } diff --git a/app/src/modules/builder/geomentries/assets/addAssetModel.ts b/app/src/modules/builder/geomentries/assets/addAssetModel.ts index c718ec3..6e978dc 100644 --- a/app/src/modules/builder/geomentries/assets/addAssetModel.ts +++ b/app/src/modules/builder/geomentries/assets/addAssetModel.ts @@ -186,22 +186,59 @@ async function handleModelLoad( state: "idle", type: 'transfer', speed: 1, - points: data.points.map((point: THREE.Vector3, index: number) => ({ - uuid: THREE.MathUtils.generateUUID(), - position: [point.x, point.y, point.z], - rotation: [0, 0, 0], - action: { - actionUuid: THREE.MathUtils.generateUUID(), - actionName: `Action ${index}`, - actionType: 'default', - material: 'Default Material', - delay: 0, - spawnInterval: 5, - spawnCount: 1, - triggers: [] + points: data.points.map((point: THREE.Vector3, index: number) => { + const triggers: TriggerSchema[] = []; + + if (data.points && index < data.points.length - 1) { + triggers.push({ + triggerUuid: THREE.MathUtils.generateUUID(), + triggerName: `Trigger 1`, + triggerType: "onComplete", + delay: 0, + triggeredAsset: { + triggeredModel: { + modelName: newFloorItem.modelName, + modelUuid: newFloorItem.modelUuid + }, + triggeredPoint: { + pointName: `Point`, + pointUuid: "" + }, + triggeredAction: { + actionName: `Action 1`, + actionUuid: "" + } + } + }); } - })) + + return { + uuid: THREE.MathUtils.generateUUID(), + position: [point.x, point.y, point.z], + rotation: [0, 0, 0], + action: { + actionUuid: THREE.MathUtils.generateUUID(), + actionName: `Action 1`, + actionType: 'default', + material: 'Default Material', + delay: 0, + spawnInterval: 5, + spawnCount: 1, + triggers: triggers + } + }; + }) }; + + for (let i = 0; i < ConveyorEvent.points.length - 1; i++) { + const currentPoint = ConveyorEvent.points[i]; + const nextPoint = ConveyorEvent.points[i + 1]; + + if (currentPoint.action.triggers.length > 0) { + currentPoint.action.triggers[0].triggeredAsset!.triggeredPoint!.pointUuid = nextPoint.uuid; + currentPoint.action.triggers[0].triggeredAsset!.triggeredAction!.actionUuid = nextPoint.action.actionUuid; + } + } addEvent(ConveyorEvent); eventData.points = ConveyorEvent.points.map(point => ({ uuid: point.uuid, @@ -228,7 +265,7 @@ async function handleModelLoad( actionType: "travel", unLoadDuration: 5, loadCapacity: 10, - steeringAngle:0, + steeringAngle: 0, pickUpPoint: null, unLoadPoint: null, triggers: [] diff --git a/app/src/modules/builder/geomentries/assets/assetManager.ts b/app/src/modules/builder/geomentries/assets/assetManager.ts index 5f7798e..889d905 100644 --- a/app/src/modules/builder/geomentries/assets/assetManager.ts +++ b/app/src/modules/builder/geomentries/assets/assetManager.ts @@ -18,7 +18,7 @@ export default async function assetManager( const taskId = ++currentTaskId; // Increment taskId for each call activePromises.set(taskId, true); // Mark task as active - // console.log("Received message from worker:", data); + // if (data.toRemove.length > 0) { data.toRemove.forEach((uuid: string) => { @@ -58,7 +58,7 @@ export default async function assetManager( // Check Three.js Cache const cachedModel = THREE.Cache.get(item.modelfileID!); if (cachedModel) { - // console.log(`[Cache] Fetching ${item.modelName}`); + // processLoadedModel(cachedModel.scene.clone(), item, itemsGroup, resolve); return; } @@ -66,7 +66,7 @@ export default async function assetManager( // Check IndexedDB const indexedDBModel = await retrieveGLTF(item.modelfileID!); if (indexedDBModel) { - // console.log(`[IndexedDB] Fetching ${item.modelName}`); + // const blobUrl = URL.createObjectURL(indexedDBModel); loader.load( blobUrl, @@ -86,7 +86,7 @@ export default async function assetManager( } // Fetch from Backend - // console.log(`[Backend] Fetching ${item.modelName}`); + // loader.load( modelUrl, async (gltf) => { @@ -114,14 +114,14 @@ export default async function assetManager( const existingModel = itemsGroup?.current?.getObjectByProperty("uuid", item.modelUuid); if (existingModel) { - // console.log(`Model ${item.modelName} already exists in the scene.`); + // resolve(); return; } const model = gltf; model.uuid = item.modelUuid; - model.userData = { name: item.modelName, modelId: item.modelfileID, modelUuid: item.modelUuid }; + model.userData = { name: item.modelName, modelId: item.modelfileID, modelUuid: item.modelUuid, eventData: item.eventData }; model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); model.position.set(...item.position); model.rotation.set(item.rotation.x, item.rotation.y, item.rotation.z); diff --git a/app/src/modules/scene/controls/selectionControls/moveControls.tsx b/app/src/modules/scene/controls/selectionControls/moveControls.tsx index d371806..5b33c14 100644 --- a/app/src/modules/scene/controls/selectionControls/moveControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/moveControls.tsx @@ -1,7 +1,7 @@ import * as THREE from "three"; -import { useEffect, useMemo, useRef } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useFrame, useThree } from "@react-three/fiber"; -import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; +import { useFloorItems, useSelectedAssets, useSocketStore, useStartSimulation, useToggleView } from "../../../../store/store"; // import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; import { toast } from "react-toastify"; import * as Types from "../../../../types/world/worldTypes"; @@ -9,6 +9,8 @@ import { detectModifierKeys } from "../../../../utils/shortcutkeys/detectModifie import { useEventsStore } from "../../../../store/simulation/useEventsStore"; import { useProductStore } from "../../../../store/simulation/useProductStore"; import { useSelectedProduct } from "../../../../store/simulation/useSimulationStore"; +import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertProductOrEventApi"; +import { snapControls } from "../../../../utils/handleSnap"; function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, duplicatedObjects, setDuplicatedObjects, selectionGroup, rotatedObjects, setRotatedObjects, boundingBoxRef }: any) { const { camera, controls, gl, scene, pointer, raycaster } = useThree(); @@ -16,9 +18,28 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje const { toggleView } = useToggleView(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const { selectedProduct } = useSelectedProduct(); const { floorItems, setFloorItems } = useFloorItems(); const { socket } = useSocketStore(); const itemsData = useRef([]); + const [keyEvent, setKeyEvent] = useState<"Ctrl" | "Shift" | "Ctrl+Shift" | "">("") + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData + }) + } useEffect(() => { if (!camera || !scene || toggleView || !itemsGroupRef.current) return; @@ -35,6 +56,15 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje const onPointerMove = () => { isMoving = true; }; + const onKeyUp = (event: KeyboardEvent) => { + // When any modifier is released, reset snap + const isModifierKey = + event.key === "Control" || event.key === "Shift"; + + if (isModifierKey) { + setKeyEvent(""); + } + }; const onPointerUp = (event: PointerEvent) => { if (!isMoving && movedObjects.length > 0 && event.button === 0) { @@ -56,18 +86,28 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje setMovedObjects([]); itemsData.current = []; } + setKeyEvent("") }; const onKeyDown = (event: KeyboardEvent) => { const keyCombination = detectModifierKeys(event); if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0) return; + + if (keyCombination === "Ctrl" || keyCombination === "Ctrl+Shift" || keyCombination === "Shift") { + // update state here + setKeyEvent(keyCombination) + } else { + setKeyEvent("") + } + if (keyCombination === "G") { if (selectedAssets.length > 0) { moveAssets(); itemsData.current = floorItems.filter((item: { modelUuid: string }) => selectedAssets.some((asset: any) => asset.uuid === item.modelUuid)); } } + if (keyCombination === "ESCAPE") { event.preventDefault(); @@ -90,6 +130,7 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje canvasElement.addEventListener("pointermove", onPointerMove); canvasElement.addEventListener("pointerup", onPointerUp); canvasElement.addEventListener("keydown", onKeyDown); + canvasElement?.addEventListener("keyup", onKeyUp); } return () => { @@ -97,12 +138,11 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje canvasElement.removeEventListener("pointermove", onPointerMove); canvasElement.removeEventListener("pointerup", onPointerUp); canvasElement.removeEventListener("keydown", onKeyDown); + canvasElement?.removeEventListener("keyup", onKeyUp); }; - }, [camera, controls, scene, toggleView, selectedAssets, socket, floorItems, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects]); + }, [camera, controls, scene, toggleView, selectedAssets, socket, floorItems, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects, keyEvent]); - const gridSize = 0.25; - const moveSpeed = 0.25; - const isGridSnap = false; + let moveSpeed = keyEvent === "Ctrl" || "Ctrl+Shift" ? 1 : 0.25; useFrame(() => { if (movedObjects.length > 0) { @@ -113,10 +153,17 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje if (point) { let targetX = point.x; let targetZ = point.z; + if (keyEvent === "Ctrl") { + targetX = snapControls(targetX, "Ctrl"); + targetZ = snapControls(targetZ, "Ctrl"); + } else if (keyEvent === "Ctrl+Shift") { + targetX = snapControls(targetX, "Ctrl+Shift"); + targetZ = snapControls(targetZ, "Ctrl+Shift"); + } else if (keyEvent === "Shift") { + targetX = snapControls(targetX, "Shift"); + targetZ = snapControls(targetZ, "Shift"); + } else { - if (isGridSnap) { - targetX = Math.round(point.x / gridSize) * gridSize; - targetZ = Math.round(point.z / gridSize) * gridSize; } const position = new THREE.Vector3(); @@ -190,10 +237,21 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje }) } if (productData) { - useProductStore.getState().updateEvent(useSelectedProduct.getState().selectedProduct.productId, obj.userData.modelUuid, { + const event = useProductStore.getState().updateEvent(useSelectedProduct.getState().selectedProduct.productId, obj.userData.modelUuid, { position: [worldPosition.x, worldPosition.y, worldPosition.z], rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], }) + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } + + newFloorItem.eventData = eventData; } } @@ -203,9 +261,6 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje return updatedItems; }); - const email = localStorage.getItem("email"); - const organization = email ? email.split("@")[1].split(".")[0] : "default"; - //REST // await setFloorItemApi( @@ -253,6 +308,7 @@ function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObje setMovedObjects([]); setRotatedObjects([]); setSelectedAssets([]); + setKeyEvent("") } return null; diff --git a/app/src/modules/scene/controls/selectionControls/rotateControls.tsx b/app/src/modules/scene/controls/selectionControls/rotateControls.tsx index 08667b4..58dab0c 100644 --- a/app/src/modules/scene/controls/selectionControls/rotateControls.tsx +++ b/app/src/modules/scene/controls/selectionControls/rotateControls.tsx @@ -8,6 +8,7 @@ import * as Types from "../../../../types/world/worldTypes"; import { useEventsStore } from "../../../../store/simulation/useEventsStore"; import { useProductStore } from "../../../../store/simulation/useProductStore"; import { useSelectedProduct } from "../../../../store/simulation/useSimulationStore"; +import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertProductOrEventApi"; function RotateControls({ rotatedObjects, setRotatedObjects, movedObjects, setMovedObjects, itemsGroupRef, copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, duplicatedObjects, setDuplicatedObjects, selectionGroup, boundingBoxRef }: any) { const { camera, controls, gl, scene, pointer, raycaster } = useThree(); @@ -15,10 +16,28 @@ function RotateControls({ rotatedObjects, setRotatedObjects, movedObjects, setMo const { toggleView } = useToggleView(); const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const { selectedProduct } = useSelectedProduct(); const { floorItems, setFloorItems } = useFloorItems(); const { socket } = useSocketStore(); const itemsData = useRef([]); + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData + }) + } + const prevPointerPosition = useRef(null); useEffect(() => { @@ -190,10 +209,21 @@ function RotateControls({ rotatedObjects, setRotatedObjects, movedObjects, setMo }) } if (productData) { - useProductStore.getState().updateEvent(useSelectedProduct.getState().selectedProduct.productId, obj.userData.modelUuid, { + const event = useProductStore.getState().updateEvent(useSelectedProduct.getState().selectedProduct.productId, obj.userData.modelUuid, { position: [worldPosition.x, worldPosition.y, worldPosition.z], rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], }) + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } + + newFloorItem.eventData = eventData; } } @@ -203,9 +233,6 @@ function RotateControls({ rotatedObjects, setRotatedObjects, movedObjects, setMo return updatedItems; }); - const email = localStorage.getItem("email"); - const organization = email ? email.split("@")[1].split(".")[0] : "default"; - //REST // await setFloorItemApi( diff --git a/app/src/modules/simulation/events/points/creator/pointsCreator.tsx b/app/src/modules/simulation/events/points/creator/pointsCreator.tsx index c6ec316..0ed7e13 100644 --- a/app/src/modules/simulation/events/points/creator/pointsCreator.tsx +++ b/app/src/modules/simulation/events/points/creator/pointsCreator.tsx @@ -1,28 +1,36 @@ import React, { useEffect, useRef, useState } from "react"; import * as THREE from "three"; import { useEventsStore } from "../../../../../store/simulation/useEventsStore"; -import useModuleStore from "../../../../../store/useModuleStore"; +import useModuleStore, { useSubModuleStore } from "../../../../../store/useModuleStore"; import { TransformControls } from "@react-three/drei"; import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys"; import { useSelectedEventSphere, useSelectedEventData, + useIsDragging, + useIsRotating, } from "../../../../../store/simulation/useSimulationStore"; +import { useThree } from "@react-three/fiber"; function PointsCreator() { + const { gl, raycaster, scene, pointer, camera } = useThree(); + const { subModule } = useSubModuleStore(); const { events, updatePoint, getPointByUuid, getEventByModelUuid } = useEventsStore(); const { activeModule } = useModuleStore(); const transformRef = useRef(null); const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null); const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({}); const { selectedEventSphere, setSelectedEventSphere, clearSelectedEventSphere, } = useSelectedEventSphere(); - const { selectedEventData, setSelectedEventData, clearSelectedEventData } = useSelectedEventData(); + const { selectedEventData,setSelectedEventData, clearSelectedEventData } = useSelectedEventData(); + const { isDragging } = useIsDragging(); + const { isRotating } = useIsRotating(); useEffect(() => { if (selectedEventSphere) { const eventData = getEventByModelUuid( selectedEventSphere.userData.modelUuid ); + if (eventData) { setSelectedEventData(eventData, selectedEventSphere.userData.pointUuid); } else { @@ -72,6 +80,53 @@ function PointsCreator() { } }; + useEffect(() => { + const canvasElement = gl.domElement; + + let drag = false; + let isMouseDown = false; + + const onMouseDown = () => { + isMouseDown = true; + drag = false; + }; + + const onMouseUp = () => { + if (selectedEventSphere && !drag) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster + .intersectObjects(scene.children, true) + .filter( + (intersect) => + intersect.object.name === ('Event-Sphere') + ); + if (intersects.length === 0) { + clearSelectedEventSphere(); + setTransformMode(null); + } + } + } + + const onMouseMove = () => { + if (isMouseDown) { + drag = true; + } + }; + + if (subModule === 'mechanics') { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + } + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + }; + + }, [gl, subModule, selectedEventSphere]); + return ( <> {activeModule === "simulation" && ( @@ -80,7 +135,10 @@ function PointsCreator() { {events.map((event, i) => { if (event.type === "transfer") { return ( - + {event.points.map((point, j) => ( { if (selectedEventData?.data.type !== 'vehicle') { - clearSelectedEventSphere(); + // clearSelectedEventSphere(); } setTransformMode(null); }} key={`${i}-${j}`} position={new THREE.Vector3(...point.position)} - // rotation={new THREE.Euler(...point.rotation)} - userData={{ modelUuid: event.modelUuid, pointUuid: point.uuid }} + userData={{ + modelUuid: event.modelUuid, + pointUuid: point.uuid, + }} > @@ -111,7 +171,10 @@ function PointsCreator() { ); } else if (event.type === "vehicle") { return ( - + { - clearSelectedEventSphere(); - setTransformMode(null); - }} position={new THREE.Vector3(...event.point.position)} - // rotation={new THREE.Euler(...event.point.rotation)} - userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }} + userData={{ + modelUuid: event.modelUuid, + pointUuid: event.point.uuid, + }} > @@ -137,7 +198,10 @@ function PointsCreator() { ); } else if (event.type === "roboticArm") { return ( - + { - clearSelectedEventSphere(); setTransformMode(null); }} position={new THREE.Vector3(...event.point.position)} - // rotation={new THREE.Euler(...event.point.rotation)} - userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }} + userData={{ + modelUuid: event.modelUuid, + pointUuid: event.point.uuid, + }} > @@ -163,7 +228,10 @@ function PointsCreator() { ); } else if (event.type === "machine") { return ( - + { - clearSelectedEventSphere(); + // clearSelectedEventSphere(); setTransformMode(null); }} position={new THREE.Vector3(...event.point.position)} - // rotation={new THREE.Euler(...event.point.rotation)} - userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }} + userData={{ + modelUuid: event.modelUuid, + pointUuid: event.point.uuid, + }} > @@ -208,4 +278,4 @@ function PointsCreator() { ); } -export default PointsCreator; +export default PointsCreator; \ No newline at end of file diff --git a/app/src/modules/simulation/machine/instances/animator/machineAnimator.tsx b/app/src/modules/simulation/machine/instances/animator/machineAnimator.tsx new file mode 100644 index 0000000..6ef2b0e --- /dev/null +++ b/app/src/modules/simulation/machine/instances/animator/machineAnimator.tsx @@ -0,0 +1,102 @@ +import { useFrame } from '@react-three/fiber'; +import React, { useEffect, useRef } from 'react'; +import { useMachineStore } from '../../../../../store/simulation/useMachineStore'; +import { useAnimationPlaySpeed, usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore'; + + +interface MachineAnimatorProps { + currentPhase: string; + handleCallBack: () => void; + reset: () => void; + machineStatus: (modelId: string, status: string) => void; + processingTime: number; + machineUuid: string +} + +const MachineAnimator = ({ currentPhase, handleCallBack, processingTime, machineUuid, machineStatus, reset }: MachineAnimatorProps) => { + const animationStarted = useRef(false); + const isPausedRef = useRef(false); + const startTimeRef = useRef(0); + const animationFrameId = useRef(null); + const pauseTimeRef = useRef(null); + const { isPaused } = usePauseButtonStore(); + const { removeCurrentAction } = useMachineStore(); + const { isReset, setReset } = useResetButtonStore(); + const { isPlaying } = usePlayButtonStore(); + const { speed } = useAnimationPlaySpeed(); + const isPlayingRef = useRef(false); + const isResetRef = useRef(false) + + + useEffect(() => { + isPausedRef.current = isPaused; + }, [isPaused]); + useEffect(() => { + isPlayingRef.current = isPlaying; + }, [isPlaying]); + useEffect(() => { + isResetRef.current = isReset; + }, [isReset]); + + + useEffect(() => { + + if (isReset || !isPlaying) { + reset(); + setReset(false); + startTimeRef.current = 0; + isPausedRef.current = false; + pauseTimeRef.current = 0; + animationFrameId.current = null; + animationStarted.current = false; + removeCurrentAction(machineUuid) + } + }, [isReset, isPlaying]) + + useEffect(() => { + if (currentPhase === 'processing' && !animationStarted.current && machineUuid) { + animationStarted.current = true; + startTimeRef.current = performance.now(); + animationFrameId.current = requestAnimationFrame(step); + } + }, [currentPhase]); + + function step(time: number) { + if (!isPausedRef.current || !isResetRef.current) { + if (animationFrameId.current) { + cancelAnimationFrame(animationFrameId.current); + animationFrameId.current = null; + } + if (isPausedRef.current) { + if (!pauseTimeRef.current) { + pauseTimeRef.current = performance.now(); + } + animationFrameId.current = requestAnimationFrame(step); + return; + } + + if (pauseTimeRef.current) { + const pauseDuration = performance.now() - pauseTimeRef.current; + startTimeRef.current += pauseDuration; + pauseTimeRef.current = null; + } + + const elapsed = time - startTimeRef.current; + const processedTime = processingTime * 1000; + if (elapsed < processedTime) { + machineStatus(machineUuid, "Machine is currently processing the task"); + animationFrameId.current = requestAnimationFrame(step); + } else { + removeCurrentAction(machineUuid); + animationStarted.current = false; + handleCallBack(); + + } + } + } + + + return null; +} + +export default MachineAnimator; diff --git a/app/src/modules/simulation/machine/instances/machineInstance/machineInstance.tsx b/app/src/modules/simulation/machine/instances/machineInstance/machineInstance.tsx index edb825f..714bcdb 100644 --- a/app/src/modules/simulation/machine/instances/machineInstance/machineInstance.tsx +++ b/app/src/modules/simulation/machine/instances/machineInstance/machineInstance.tsx @@ -1,8 +1,65 @@ -import React from 'react' +import React, { useEffect, useRef, useState } from 'react' +import { useMachineStore } from '../../../../../store/simulation/useMachineStore'; +import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; +import MachineAnimator from '../animator/machineAnimator'; + +function MachineInstance({ machineDetail }: any) { + const [currentPhase, setCurrentPhase] = useState('idle'); + let isIncrememtable = useRef(true); + const { isPlaying } = usePlayButtonStore(); + const { machines, addCurrentAction, setMachineState, setMachineActive } = useMachineStore(); + + const reset = () => { + setMachineState(machineDetail.modelUuid, 'idle'); + setMachineActive(machineDetail.modelUuid, false); + isIncrememtable.current = true; + setCurrentPhase("idle"); + } + const increment = () => { + if (isIncrememtable.current) { + addCurrentAction(machineDetail.modelUuid, "machine-action-2468-1357-8024") + isIncrememtable.current = false; + } + } + function machineStatus(modelId: string, status: string) { + // console.log(`${modelId} , ${status}`); + + } + + useEffect(() => { + if (isPlaying) { + if (!machineDetail.isActive && machineDetail.state === "idle" && currentPhase == "idle" && !machineDetail.currentAction) { + setTimeout(() => { + increment(); + }, 2000); + machineStatus(machineDetail.modelUuid, 'Machine is idle and waiting for next instruction.') + } else if (!machineDetail.isActive && machineDetail.state === "idle" && currentPhase == "idle" && machineDetail.currentAction) { + setCurrentPhase("processing"); + setMachineState(machineDetail.modelUuid, 'running'); + setMachineActive(machineDetail.modelUuid, true); + machineStatus(machineDetail.modelUuid, "Machine started processing") + } + } else { + reset(); + } + }, [currentPhase, isPlaying, machines]) + + function handleCallBack() { + if (currentPhase == "processing") { + setMachineState(machineDetail.modelUuid, 'idle'); + setMachineActive(machineDetail.modelUuid, false); + setCurrentPhase("idle") + isIncrememtable.current = true; + machineStatus(machineDetail.modelUuid, "Machine has completed the processing") + } + } + // console.log('currentPhase: ', currentPhase); + + -function MachineInstance() { return ( <> + ) } diff --git a/app/src/modules/simulation/machine/instances/machineInstances.tsx b/app/src/modules/simulation/machine/instances/machineInstances.tsx index b0c2c9f..8536cac 100644 --- a/app/src/modules/simulation/machine/instances/machineInstances.tsx +++ b/app/src/modules/simulation/machine/instances/machineInstances.tsx @@ -1,11 +1,14 @@ import React from 'react' import MachineInstance from './machineInstance/machineInstance' +import { useMachineStore } from '../../../../store/simulation/useMachineStore'; function MachineInstances() { + const { machines } = useMachineStore(); return ( <> - - + {machines.map((val: MachineStatus) => ( + + ))} ) diff --git a/app/src/modules/simulation/machine/machine.tsx b/app/src/modules/simulation/machine/machine.tsx index 32c3b8e..3d24f61 100644 --- a/app/src/modules/simulation/machine/machine.tsx +++ b/app/src/modules/simulation/machine/machine.tsx @@ -1,7 +1,49 @@ -import React from 'react' +import React, { useEffect } from 'react' import MachineInstances from './instances/machineInstances' +import { useMachineStore } from '../../../store/simulation/useMachineStore' +import { useSelectedProduct } from '../../../store/simulation/useSimulationStore'; function Machine() { + const { addMachine, addCurrentAction, removeMachine, machines } = useMachineStore(); + const { selectedProduct } = useSelectedProduct(); + + const machineSample: MachineEventSchema[] = [ + { + modelUuid: "machine-1234-5678-9012", + modelName: "CNC Milling Machine", + position: [10, 0, 5], + rotation: [0, 0, 0], + state: "idle", + type: "machine", + point: { + uuid: "machine-point-9876-5432-1098", + position: [10, 0.5, 5.2], + rotation: [0, 0, 0], + action: { + actionUuid: "machine-action-2468-1357-8024", + actionName: "Metal Processing", + actionType: "process", + processTime: 10, + swapMaterial: "steel", + triggers: [] + } + } + } + ]; + + useEffect(() => { + removeMachine(machineSample[0].modelUuid); + addMachine(selectedProduct.productId, machineSample[0]); + + // addCurrentAction(machineSample[0].modelUuid, machineSample[0].point.action.actionUuid); + }, []) + + + useEffect(() => { + + // console.log('machines: ', machines); + }, [machines]) + return ( <> diff --git a/app/src/modules/simulation/products/events/addOrRemoveEventsInProducts.tsx b/app/src/modules/simulation/products/events/addOrRemoveEventsInProducts.tsx index 9eececc..715aa37 100644 --- a/app/src/modules/simulation/products/events/addOrRemoveEventsInProducts.tsx +++ b/app/src/modules/simulation/products/events/addOrRemoveEventsInProducts.tsx @@ -53,7 +53,17 @@ function AddOrRemoveEventsInProducts() { const canvasElement = gl.domElement; if (!canvasElement) return; - let intersects = raycaster.intersectObjects(scene.children, true); + const intersects = raycaster + .intersectObjects(scene.children, true) + .filter( + (intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.name.includes("agv-collider") && + !(intersect.object.type === "GridHelper") && + !(intersect.object?.parent?.name.includes('zones')) && + !(intersect.object?.parent?.name.includes('Zone')) + ); if (intersects.length > 0 && intersects[0]?.object?.parent?.parent?.position && intersects[0]?.object?.parent?.parent?.scale && intersects[0]?.object?.parent?.parent?.rotation) { let currentObject = intersects[0].object; @@ -116,6 +126,7 @@ function AddOrRemoveEventsInProducts() { }; }, [gl, subModule, selectedProduct, selectedAsset]); + return ( <> ) diff --git a/app/src/modules/simulation/products/products.tsx b/app/src/modules/simulation/products/products.tsx index ee1ac42..921dd96 100644 --- a/app/src/modules/simulation/products/products.tsx +++ b/app/src/modules/simulation/products/products.tsx @@ -7,7 +7,7 @@ import { upsertProductOrEventApi } from '../../../services/simulation/UpsertProd import { getAllProductsApi } from '../../../services/simulation/getallProductsApi'; function Products() { - const { products, addProduct, setProducts } = useProductStore(); + const { addProduct, setProducts } = useProductStore(); const { setSelectedProduct } = useSelectedProduct(); useEffect(() => { @@ -27,9 +27,6 @@ function Products() { }) }, []) - useEffect(() => { - }, []) - return ( <> diff --git a/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx b/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx index c82b73d..10f90bf 100644 --- a/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx +++ b/app/src/modules/simulation/roboticArm/instances/animator/roboticArmAnimator.tsx @@ -1,68 +1,220 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react' -import { useArmBotStore } from '../../../../../store/simulation/useArmBotStore'; -import { useFrame, useThree } from '@react-three/fiber'; -import * as THREE from "three" -import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { useFrame } from '@react-three/fiber'; +import * as THREE from 'three'; +import { Line } from '@react-three/drei'; +import { + useAnimationPlaySpeed, + usePauseButtonStore, + usePlayButtonStore, + useResetButtonStore +} from '../../../../../store/usePlayButtonStore'; -function RoboticArmAnimator({ armUuid, HandleCallback, currentPhase, ikSolver, targetBone, robot, logStatus, groupRef, processes, armBotCurveRef, path }: any) { - const { armBots } = useArmBotStore(); - const { scene } = useThree(); - const restSpeed = 0.1; - const restPosition = new THREE.Vector3(0, 1, -1.6); - const initialCurveRef = useRef(null); - const initialStartPositionRef = useRef(null); - const [initialProgress, setInitialProgress] = useState(0); - const [progress, setProgress] = useState(0); - const [needsInitialMovement, setNeedsInitialMovement] = useState(true); - const [isInitializing, setIsInitializing] = useState(true); - const { isPlaying } = usePlayButtonStore(); - const statusRef = useRef("idle"); - // Create a ref for initialProgress - const initialProgressRef = useRef(0); +function RoboticArmAnimator({ + HandleCallback, + restPosition, + ikSolver, + targetBone, + armBot, + logStatus, + path +}: any) { + const progressRef = useRef(0); + const curveRef = useRef(null); const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]); + const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]); + const [customCurvePoints, setCustomCurvePoints] = useState(null); + // Zustand stores + const { isPlaying } = usePlayButtonStore(); + const { isPaused } = usePauseButtonStore(); + const { isReset } = useResetButtonStore(); + const { speed } = useAnimationPlaySpeed(); + + // Update path state whenever `path` prop changes useEffect(() => { - - setCurrentPath(path) - }, [path]) + setCurrentPath(path); + }, [path]); + // Reset logic when `isPlaying` changes useEffect(() => { + if (!isPlaying) { + setCurrentPath([]); + curveRef.current = null; + } + }, [isPlaying]); - }, [currentPath]) + // Handle circle points based on armBot position + useEffect(() => { + const points = generateRingPoints(1.6, 64) + setCirclePoints(points); + }, [armBot.position]); + + function generateRingPoints(radius: any, segments: any) { + const points: [number, number, number][] = []; + for (let i = 0; i < segments; i++) { + // Calculate angle for current segment + const angle = (i / segments) * Math.PI * 2; + // Calculate x and z coordinates (y remains the same for a flat ring) + const x = Math.cos(angle) * radius; + const z = Math.sin(angle) * radius; + points.push([x, 1.5, z]); + } + return points; + } + + const findNearestIndex = (nearestPoint: [number, number, number], points: [number, number, number][], epsilon = 1e-6) => { + for (let i = 0; i < points.length; i++) { + const [x, y, z] = points[i]; + if ( + Math.abs(x - nearestPoint[0]) < epsilon && + Math.abs(y - nearestPoint[1]) < epsilon && + Math.abs(z - nearestPoint[2]) < epsilon + ) { + return i; // Found the matching index + } + } + return -1; // Not found + }; + + + // Handle nearest points and final path (including arc points) + useEffect(() => { + if (circlePoints.length > 0 && currentPath.length > 0) { + const start = currentPath[0]; + const end = currentPath[currentPath.length - 1]; + + const raisedStart = [start[0], start[1] + 0.5, start[2]] as [number, number, number]; + const raisedEnd = [end[0], end[1] + 0.5, end[2]] as [number, number, number]; + + const findNearest = (target: [number, number, number]) => { + return circlePoints.reduce((nearest, point) => { + const distance = Math.hypot( + target[0] - point[0], + target[1] - point[1], + target[2] - point[2] + ); + const nearestDistance = Math.hypot( + target[0] - nearest[0], + target[1] - nearest[1], + target[2] - nearest[2] + ); + return distance < nearestDistance ? point : nearest; + }, circlePoints[0]); + }; + + const nearestToStart = findNearest(raisedStart); + + const nearestToEnd = findNearest(raisedEnd); + + const indexOfNearestStart = findNearestIndex(nearestToStart, circlePoints); + + const indexOfNearestEnd = findNearestIndex(nearestToEnd, circlePoints); + + // Find clockwise and counter-clockwise distances + const clockwiseDistance = (indexOfNearestEnd - indexOfNearestStart + 64) % 64; + + const counterClockwiseDistance = (indexOfNearestStart - indexOfNearestEnd + 64) % 64; + + const clockwiseIsShorter = clockwiseDistance <= counterClockwiseDistance; + + // Collect arc points between start and end + let arcPoints: [number, number, number][] = []; + + if (clockwiseIsShorter) { + if (indexOfNearestStart <= indexOfNearestEnd) { + arcPoints = circlePoints.slice(indexOfNearestStart, indexOfNearestEnd + 1); + } else { + // Wrap around + arcPoints = [ + ...circlePoints.slice(indexOfNearestStart, 64), + ...circlePoints.slice(0, indexOfNearestEnd + 1) + ]; + } + } else { + if (indexOfNearestStart >= indexOfNearestEnd) { + for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) { + arcPoints.push(circlePoints[i]); + } + } else { + for (let i = indexOfNearestStart; i !== (indexOfNearestEnd - 1 + 64) % 64; i = (i - 1 + 64) % 64) { + arcPoints.push(circlePoints[i]); + } + } + } + + // Continue your custom path logic + const pathVectors = [ + new THREE.Vector3(start[0], start[1], start[2]), // start + new THREE.Vector3(raisedStart[0], raisedStart[1], raisedStart[2]), // lift up + new THREE.Vector3(nearestToStart[0], raisedStart[1], nearestToStart[2]), // move to arc start + ...arcPoints.map(point => new THREE.Vector3(point[0], raisedStart[1], point[2])), + new THREE.Vector3(nearestToEnd[0], raisedEnd[1], nearestToEnd[2]), // move from arc end + new THREE.Vector3(raisedEnd[0], raisedEnd[1], raisedEnd[2]), // lowered end + new THREE.Vector3(end[0], end[1], end[2]) // end + ]; + + const customCurve = new THREE.CatmullRomCurve3(pathVectors, false, 'centripetal', 1); + const generatedPoints = customCurve.getPoints(100); + setCustomCurvePoints(generatedPoints); + } + }, [circlePoints, currentPath]); + + // Frame update for animation useFrame((_, delta) => { - if (!ikSolver || !currentPath || currentPath.length === 0) return; + if (!ikSolver) return; - const bone = ikSolver.mesh.skeleton.bones.find( - (b: any) => b.name === targetBone - ); + const bone = ikSolver.mesh.skeleton.bones.find((b: any) => b.name === targetBone); if (!bone) return; - // Ensure currentPath is a valid array of 3D points, create a CatmullRomCurve3 from it - const curve = new THREE.CatmullRomCurve3( - currentPath.map(point => new THREE.Vector3(point[0], point[1], point[2])) - ); + if (isPlaying) { + if (!isPaused && customCurvePoints && currentPath.length > 0) { + const curvePoints = customCurvePoints; + const speedAdjustedProgress = progressRef.current + (speed * armBot.speed); + const index = Math.floor(speedAdjustedProgress); + if (index >= curvePoints.length) { + // Reached the end of the curve + HandleCallback(); + setCurrentPath([]); + curveRef.current = null; + progressRef.current = 0; + } else { + const point = curvePoints[index]; + bone.position.copy(point); + progressRef.current = speedAdjustedProgress; + } + } else if (isPaused) { + logStatus(armBot.modelUuid, 'Simulation Paused'); + } - const next = initialProgressRef.current + delta * 0.5; - if (next >= 1) { - // bone.position.copy(restPosition); - HandleCallback(); // Call the callback when the path is completed - initialProgressRef.current = 0; // Set ref to 1 when done - } else { - const point = curve.getPoint(next); // Get the interpolated point from the curve - bone.position.copy(point); // Update the bone position along the curve - initialProgressRef.current = next; // Update progress + ikSolver.update(); + } else if (!isPlaying && currentPath.length === 0) { + // Not playing anymore, reset to rest + bone.position.copy(restPosition); + ikSolver.update(); } - - ikSolver.update(); }); - return ( - <> - ) + <> + {customCurvePoints && currentPath && isPlaying && ( + + [p.x, p.y, p.z] as [number, number, number])} + color="green" + lineWidth={5} + dashed={false} + /> + + )} + + + + + + ); } -export default RoboticArmAnimator; \ No newline at end of file +export default RoboticArmAnimator; diff --git a/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx b/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx index 6bde587..9199ede 100644 --- a/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx +++ b/app/src/modules/simulation/roboticArm/instances/armInstance/roboticArmInstance.tsx @@ -1,61 +1,141 @@ import React, { useEffect, useRef, useState } from 'react' import IKInstance from '../ikInstance/ikInstance'; import RoboticArmAnimator from '../animator/roboticArmAnimator'; -import { usePlayButtonStore } from '../../../../../store/usePlayButtonStore'; +import { usePauseButtonStore, usePlayButtonStore, useResetButtonStore } from '../../../../../store/usePlayButtonStore'; import { useArmBotStore } from '../../../../../store/simulation/useArmBotStore'; import armModel from "../../../../../assets/gltf-glb/rigged/ik_arm_4.glb"; import { useThree } from "@react-three/fiber"; import { useFloorItems } from '../../../../../store/store'; import useModuleStore from '../../../../../store/useModuleStore'; -import { Vector3 } from "three"; import * as THREE from "three"; +import { useSelectedAction, useSelectedProduct } from '../../../../../store/simulation/useSimulationStore'; +import { useProductStore } from '../../../../../store/simulation/useProductStore'; -interface Process { - triggerId: string; - startPoint?: Vector3; - endPoint?: Vector3; - speed: number; -} -function RoboticArmInstance({ robot }: { robot: ArmBotStatus }) { +function RoboticArmInstance({ armBot }: { armBot: ArmBotStatus }) { - const { isPlaying } = usePlayButtonStore(); const [currentPhase, setCurrentPhase] = useState<(string)>("init"); - const { scene } = useThree(); - const targetBone = "Target"; - const { activeModule } = useModuleStore(); - const [ikSolver, setIkSolver] = useState(null); - const { addCurrentAction, setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore(); - const { floorItems } = useFloorItems(); - const groupRef = useRef(null); - const [processes, setProcesses] = useState([]); - const [armBotCurvePoints, setArmBotCurvePoints] = useState({ start: [], end: [] }) - const restPosition = new THREE.Vector3(0, 1, -1.6); - let armBotCurveRef = useRef(null) const [path, setPath] = useState<[number, number, number][]>([]); + const [ikSolver, setIkSolver] = useState(null); + const { scene } = useThree(); + const restPosition = new THREE.Vector3(0, 1.75, -1.6); + const targetBone = "Target"; + const groupRef = useRef(null); + const pauseTimeRef = useRef(null); + const isPausedRef = useRef(false); + let startTime: number; + //zustand + const { addCurrentAction, setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore(); + const { products, getActionByUuid } = useProductStore(); + const { selectedProduct } = useSelectedProduct(); + const { floorItems } = useFloorItems(); + const { activeModule } = useModuleStore(); + const { isPlaying } = usePlayButtonStore(); + const { isReset, setReset } = useResetButtonStore(); + const { isPaused } = usePauseButtonStore(); + const { selectedAction } = useSelectedAction(); + + function firstFrame() { + startTime = performance.now(); + step(); + } + function step() { + if (isPausedRef.current) { + if (!pauseTimeRef.current) { + pauseTimeRef.current = performance.now(); + } + requestAnimationFrame(() => step()); + return; + } + if (pauseTimeRef.current) { + const pauseDuration = performance.now() - pauseTimeRef.current; + startTime += pauseDuration; + pauseTimeRef.current = null; + } + const elapsedTime = performance.now() - startTime; + if (elapsedTime < 1500) { + // Wait until 1500ms has passed + requestAnimationFrame(step); + return; + } + if (currentPhase === "picking") { + + setArmBotActive(armBot.modelUuid, true); + setArmBotState(armBot.modelUuid, "running"); + setCurrentPhase("start-to-end"); + startTime = 0 + const startPoint = armBot.point.actions[0].process.startPoint; + const endPoint = armBot.point.actions[0].process.endPoint; + if (startPoint && endPoint) { + let curve = createCurveBetweenTwoPoints( + new THREE.Vector3(startPoint[0], startPoint[1], startPoint[2]), + new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2])); + if (curve) { + logStatus(armBot.modelUuid, "picking the object"); + setPath(curve.points.map(point => [point.x, point.y, point.z])) + } + } + logStatus(armBot.modelUuid, "Moving armBot from start point to end position.") + } else if (currentPhase === "dropping") { + + setArmBotActive(armBot.modelUuid, true); + setArmBotState(armBot.modelUuid, "running"); + setCurrentPhase("end-to-rest"); + startTime = 0; + const endPoint = armBot.point.actions[0].process.endPoint; + if (endPoint) { + + let curve = createCurveBetweenTwoPoints(new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2]), restPosition); + if (curve) { + logStatus(armBot.modelUuid, "dropping the object"); + setPath(curve.points.map(point => [point.x, point.y, point.z])); + } + } + logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.") + } + + } + useEffect(() => { + isPausedRef.current = isPaused; + }, [isPaused]); useEffect(() => { - let armItems = floorItems?.filter((val: any) => - val.modelUuid === "3abf5d46-b59e-4e6b-9c02-a4634b64b82d" - ); - // Get the first matching item - let armItem = armItems?.[0]; - if (armItem) { - const targetMesh = scene?.getObjectByProperty("uuid", armItem.modelUuid); - if (targetMesh) { - targetMesh.visible = activeModule !== "simulation" - } + const targetMesh = scene?.getObjectByProperty("uuid", armBot.modelUuid); + + if (targetMesh) { + targetMesh.visible = activeModule !== "simulation" } + const targetBones = ikSolver?.mesh.skeleton.bones.find( (b: any) => b.name === targetBone ); + if (isReset) { + logStatus(armBot.modelUuid, "Simulation Play Reset Successfully") + removeCurrentAction(armBot.modelUuid) + setArmBotActive(armBot.modelUuid, true) + setArmBotState(armBot.modelUuid, "running") + setCurrentPhase("init-to-rest"); + isPausedRef.current = false + pauseTimeRef.current = null + isPausedRef.current = false + startTime = 0 + if (targetBones) { + let curve = createCurveBetweenTwoPoints(targetBones.position, targetBones.position) + if (curve) { + setPath(curve.points.map(point => [point.x, point.y, point.z])); + } + } + setReset(false); + logStatus(armBot.modelUuid, "Moving armBot from initial point to rest position.") + } if (isPlaying) { - //Moving armBot from initial point to rest position. - if (!robot?.isActive && robot?.state == "idle" && currentPhase == "init") { - setArmBotActive(robot.modelUuid, true) - setArmBotState(robot.modelUuid, "running") + //Moving armBot from initial point to rest position. + if (!armBot?.isActive && armBot?.state == "idle" && currentPhase == "init") { + + setArmBotActive(armBot.modelUuid, true) + setArmBotState(armBot.modelUuid, "running") setCurrentPhase("init-to-rest"); if (targetBones) { let curve = createCurveBetweenTwoPoints(targetBones.position, restPosition) @@ -63,21 +143,26 @@ function RoboticArmInstance({ robot }: { robot: ArmBotStatus }) { setPath(curve.points.map(point => [point.x, point.y, point.z])); } } - logStatus(robot.modelUuid, "Moving armBot from initial point to rest position.") + logStatus(armBot.modelUuid, "Moving armBot from initial point to rest position.") } //Waiting for trigger. - else if (robot && !robot.isActive && robot.state === "idle" && currentPhase === "rest" && !robot.currentAction) { - logStatus(robot.modelUuid, "Waiting to trigger CurrentAction") - setTimeout(() => { - addCurrentAction(robot.modelUuid, 'action-003'); + else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && !armBot.currentAction) { + logStatus(armBot.modelUuid, "Waiting to trigger CurrentAction") + const timeoutId = setTimeout(() => { + addCurrentAction(armBot.modelUuid, selectedAction?.actionId); + console.log('selectedAction?.actionId: ', selectedAction?.actionId); }, 3000); + return () => clearTimeout(timeoutId); } - else if (robot && !robot.isActive && robot.state === "idle" && currentPhase === "rest" && robot.currentAction) { - if (robot.currentAction) { - setArmBotActive(robot.modelUuid, true); - setArmBotState(robot.modelUuid, "running"); + else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "rest" && armBot.currentAction) { + if (armBot.currentAction) { + + setArmBotActive(armBot.modelUuid, true); + setArmBotState(armBot.modelUuid, "running"); setCurrentPhase("rest-to-start"); - const startPoint = robot.point.actions[0].process.startPoint; + let actiondata = getActionByUuid(selectedProduct.productId, selectedAction.actionId) + console.log('actiondata: ', actiondata); + const startPoint = armBot.point.actions[0].process.startPoint; if (startPoint) { let curve = createCurveBetweenTwoPoints(restPosition, new THREE.Vector3(startPoint[0], startPoint[1], startPoint[2])); if (curve) { @@ -85,105 +170,109 @@ function RoboticArmInstance({ robot }: { robot: ArmBotStatus }) { } } } - logStatus(robot.modelUuid, "Moving armBot from rest point to start position.") + logStatus(armBot.modelUuid, "Moving armBot from rest point to start position.") } - else if (robot && !robot.isActive && robot.state === "idle" && currentPhase === "picking" && robot.currentAction) { - setArmBotActive(robot.modelUuid, true); - setArmBotState(robot.modelUuid, "running"); - setCurrentPhase("start-to-end"); - const startPoint = robot.point.actions[0].process.startPoint; - const endPoint = robot.point.actions[0].process.endPoint; - if (startPoint && endPoint) { - let curve = createCurveBetweenTwoPoints( - new THREE.Vector3(startPoint[0], startPoint[1], startPoint[2]), - new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2]) - ); - if (curve) { - setTimeout(() => { - logStatus(robot.modelUuid, "picking the object"); - setPath(curve.points.map(point => [point.x, point.y, point.z])); - }, 1500) - } - } - logStatus(robot.modelUuid, "Moving armBot from start point to end position.") + else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "picking" && armBot.currentAction) { + requestAnimationFrame(firstFrame); + // setArmBotActive(armBot.modelUuid, true); + // setArmBotState(armBot.modelUuid, "running"); + // setCurrentPhase("start-to-end"); + // const startPoint = armBot.point.actions[0].process.startPoint; + // const endPoint = armBot.point.actions[0].process.endPoint; + // if (startPoint && endPoint) { + // let curve = createCurveBetweenTwoPoints( + // new THREE.Vector3(startPoint[0], startPoint[1], startPoint[2]), + // new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2])); + // if (curve) { + // setTimeout(() => { + // logStatus(armBot.modelUuid, "picking the object"); + // setPath(curve.points.map(point => [point.x, point.y, point.z])); + // }, 1500) + // } + // } + // logStatus(armBot.modelUuid, "Moving armBot from start point to end position.") } - else if (robot && !robot.isActive && robot.state === "idle" && currentPhase === "dropping" && robot.currentAction) { - setArmBotActive(robot.modelUuid, true); - setArmBotState(robot.modelUuid, "running"); - setCurrentPhase("end-to-rest"); - const endPoint = robot.point.actions[0].process.endPoint; - if (endPoint) { - let curve = createCurveBetweenTwoPoints(new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2]), restPosition - ); - if (curve) { - setTimeout(() => { - logStatus(robot.modelUuid, "dropping the object"); - setPath(curve.points.map(point => [point.x, point.y, point.z])); - }, 1500) - } - } - logStatus(robot.modelUuid, "Moving armBot from end point to rest position.") + else if (armBot && !armBot.isActive && armBot.state === "idle" && currentPhase === "dropping" && armBot.currentAction) { + requestAnimationFrame(firstFrame); + // setArmBotActive(armBot.modelUuid, true); + // setArmBotState(armBot.modelUuid, "running"); + // setCurrentPhase("end-to-rest"); + // const endPoint = armBot.point.actions[0].process.endPoint; + // if (endPoint) { + // let curve = createCurveBetweenTwoPoints(new THREE.Vector3(endPoint[0], endPoint[1], endPoint[2]), restPosition); + // if (curve) { + // setTimeout(() => { + // logStatus(armBot.modelUuid, "dropping the object"); + // setPath(curve.points.map(point => [point.x, point.y, point.z])); + // }, 1500) + // } + // } + // logStatus(armBot.modelUuid, "Moving armBot from end point to rest position.") } + } else { + logStatus(armBot.modelUuid, "Simulation Play Stopped") + setArmBotActive(armBot.modelUuid, false) + setArmBotState(armBot.modelUuid, "idle") + setCurrentPhase("init"); + setPath([]) + removeCurrentAction(armBot.modelUuid) + } - }, [currentPhase, robot, isPlaying, ikSolver]) + }, [currentPhase, armBot, isPlaying, ikSolver, isReset]) function createCurveBetweenTwoPoints(p1: any, p2: any) { const mid = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5); - mid.y += 0.5; - + // mid.y += 0.5; const points = [p1, mid, p2]; return new THREE.CatmullRomCurve3(points); } const HandleCallback = () => { - if (robot.isActive && robot.state == "running" && currentPhase == "init-to-rest") { - logStatus(robot.modelUuid, "Callback triggered: rest"); - setArmBotActive(robot.modelUuid, false) - setArmBotState(robot.modelUuid, "idle") + if (armBot.isActive && armBot.state == "running" && currentPhase == "init-to-rest") { + logStatus(armBot.modelUuid, "Callback triggered: rest"); + setArmBotActive(armBot.modelUuid, false) + setArmBotState(armBot.modelUuid, "idle") setCurrentPhase("rest"); setPath([]) } - else if (robot.isActive && robot.state == "running" && currentPhase == "rest-to-start") { - logStatus(robot.modelUuid, "Callback triggered: pick."); - setArmBotActive(robot.modelUuid, false) - setArmBotState(robot.modelUuid, "idle") + else if (armBot.isActive && armBot.state == "running" && currentPhase == "rest-to-start") { + logStatus(armBot.modelUuid, "Callback triggered: pick."); + setArmBotActive(armBot.modelUuid, false) + setArmBotState(armBot.modelUuid, "idle") setCurrentPhase("picking"); setPath([]) } - else if (robot.isActive && robot.state == "running" && currentPhase == "start-to-end") { - logStatus(robot.modelUuid, "Callback triggered: drop."); - setArmBotActive(robot.modelUuid, false) - setArmBotState(robot.modelUuid, "idle") + else if (armBot.isActive && armBot.state == "running" && currentPhase == "start-to-end") { + logStatus(armBot.modelUuid, "Callback triggered: drop."); + setArmBotActive(armBot.modelUuid, false) + setArmBotState(armBot.modelUuid, "idle") setCurrentPhase("dropping"); setPath([]) } - else if (robot.isActive && robot.state == "running" && currentPhase == "end-to-rest") { - logStatus(robot.modelUuid, "Callback triggered: rest, cycle completed."); - setArmBotActive(robot.modelUuid, false) - setArmBotState(robot.modelUuid, "idle") + else if (armBot.isActive && armBot.state == "running" && currentPhase == "end-to-rest") { + logStatus(armBot.modelUuid, "Callback triggered: rest, cycle completed."); + setArmBotActive(armBot.modelUuid, false) + setArmBotState(armBot.modelUuid, "idle") setCurrentPhase("rest"); setPath([]) - removeCurrentAction(robot.modelUuid) + removeCurrentAction(armBot.modelUuid) } } const logStatus = (id: string, status: string) => { - // console.log(id + "," + status); - console.log( status); + // + } return ( <> - - - + + ) } -export default RoboticArmInstance; \ No newline at end of file +export default RoboticArmInstance; diff --git a/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx b/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx index 645cbb5..e692ca5 100644 --- a/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx +++ b/app/src/modules/simulation/roboticArm/instances/ikInstance/ikInstance.tsx @@ -3,20 +3,19 @@ import * as THREE from "three"; import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; import { clone } from "three/examples/jsm/utils/SkeletonUtils"; -import { useFrame, useLoader, useThree } from "@react-three/fiber"; +import { useLoader, useThree } from "@react-three/fiber"; import { CCDIKSolver, CCDIKHelper, } from "three/examples/jsm/animation/CCDIKSolver"; +import { TransformControls } from '@react-three/drei'; + type IKInstanceProps = { modelUrl: string; ikSolver: any; setIkSolver: any - robot: any; - groupRef: React.RefObject; - processes: any; - setArmBotCurvePoints: any + armBot: any; + groupRef: any; }; -function IKInstance({ modelUrl, setIkSolver, ikSolver, robot, groupRef, processes, setArmBotCurvePoints }: IKInstanceProps) { - - const { scene } = useThree(); +function IKInstance({ modelUrl, setIkSolver, ikSolver, armBot, groupRef }: IKInstanceProps) { + const { scene } = useThree() const gltf = useLoader(GLTFLoader, modelUrl, (loader) => { const draco = new DRACOLoader(); draco.setDecoderPath("https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/"); @@ -25,6 +24,8 @@ function IKInstance({ modelUrl, setIkSolver, ikSolver, robot, groupRef, processe const cloned = useMemo(() => clone(gltf?.scene), [gltf]); const targetBoneName = "Target"; const skinnedMeshName = "link_0"; + const [selectedArm, setSelectedArm] = useState(); + useEffect(() => { if (!gltf) return; const OOI: any = {}; @@ -66,15 +67,19 @@ function IKInstance({ modelUrl, setIkSolver, ikSolver, robot, groupRef, processe setIkSolver(solver); const helper = new CCDIKHelper(OOI.Skinned_Mesh, iks, 0.05) + // groupRef.current.add(helper); - // scene.add(helper) + setSelectedArm(OOI.Target_Bone); + scene.add(helper) }, [gltf]); return ( <> - + { + setSelectedArm(groupRef.current?.getObjectByName(targetBoneName)) + }}> + {/* {selectedArm && } */} ) } diff --git a/app/src/modules/simulation/roboticArm/instances/roboticArmInstances.tsx b/app/src/modules/simulation/roboticArm/instances/roboticArmInstances.tsx index 1089fa5..0acc1d9 100644 --- a/app/src/modules/simulation/roboticArm/instances/roboticArmInstances.tsx +++ b/app/src/modules/simulation/roboticArm/instances/roboticArmInstances.tsx @@ -1,4 +1,3 @@ -import React from 'react' import RoboticArmInstance from './armInstance/roboticArmInstance'; import { useArmBotStore } from '../../../../store/simulation/useArmBotStore'; @@ -8,9 +7,8 @@ function RoboticArmInstances() { return ( <> {armBots?.map((robot: ArmBotStatus) => ( - + ))} - ) } diff --git a/app/src/modules/simulation/roboticArm/roboticArm.tsx b/app/src/modules/simulation/roboticArm/roboticArm.tsx index 69e6da0..2cb9ea6 100644 --- a/app/src/modules/simulation/roboticArm/roboticArm.tsx +++ b/app/src/modules/simulation/roboticArm/roboticArm.tsx @@ -2,18 +2,26 @@ import { useEffect } from "react"; import RoboticArmInstances from "./instances/roboticArmInstances"; import { useArmBotStore } from "../../../store/simulation/useArmBotStore"; import { useFloorItems } from "../../../store/store"; +import { useSelectedEventData, useSelectedEventSphere, useSelectedProduct } from "../../../store/simulation/useSimulationStore"; +import { useProductStore } from "../../../store/simulation/useProductStore"; +import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; +import ArmBotUI from "../ui/arm/armBotUI"; function RoboticArm() { - const { armBots, addArmBot, removeArmBot } = useArmBotStore(); + const { armBots, addArmBot, clearArmBots } = useArmBotStore(); + const { products, getProductById } = useProductStore(); + const { selectedProduct } = useSelectedProduct(); + const { selectedEventSphere } = useSelectedEventSphere(); + const { selectedEventData } = useSelectedEventData(); + const { isPlaying } = usePlayButtonStore(); const { floorItems } = useFloorItems(); - const armBotStatusSample: RoboticArmEventSchema[] = [ { state: "idle", - modelUuid: "3abf5d46-b59e-4e6b-9c02-a4634b64b82d", + modelUuid: "8790b4d5-fb6e-49e0-8161-04945fe3fdc4", modelName: "ArmBot-X200", - position: [0.20849215906958463, 0, 0.32079278127773675], + position: [4.317833205016363, 0, -3.2829924989068715], rotation: [-1.3768690876192207e-15, 1.4883085074751308, 1.5407776675834467e-15], type: "roboticArm", speed: 1.5, @@ -29,19 +37,9 @@ function RoboticArm() { process: { startPoint: [-1, 2, 1], endPoint: [-2, 1, -1], + // startPoint: [-2, 1, -1], + // endPoint: [-1, 2, 1], }, - // process: { - // "startPoint": [ - // 0.37114476008711866, - // 1.9999999999999998, - // 1.8418816116721384 - // ], - // "endPoint": [ - // -0.42197069459490777, - // 1, - // -3.159515927851809 - // ] - // }, triggers: [ { triggerUuid: "trigger-001", @@ -95,7 +93,7 @@ function RoboticArm() { position: [95.94347308985614, 0, 6.742905194869091], rotation: [0, 0, 0], type: "roboticArm", - speed: 1.5, + speed: 0.1, point: { uuid: "point-123", position: [0, 1.5, 0], @@ -106,8 +104,10 @@ function RoboticArm() { actionName: "Pick Component", actionType: "pickAndPlace", process: { - startPoint: [2.52543010919071, 0, 8.433681161200905], - endPoint: [95.3438373267953, 0, 9.0279187421610025], + // startPoint: [2.52543010919071, 0, 8.433681161200905], + // endPoint: [95.3438373267953, 0, 9.0279187421610025], + startPoint: null, + endPoint: null, }, triggers: [ { @@ -158,20 +158,32 @@ function RoboticArm() { ]; useEffect(() => { - - removeArmBot(armBotStatusSample[0].modelUuid); - addArmBot('123', armBotStatusSample[0]); - // addArmBot('123', armBotStatusSample[1]); - // addCurrentAction('armbot-xyz-001', 'action-001'); - }, []); + if (selectedProduct.productId) { + const product = getProductById(selectedProduct.productId); + if (product) { + clearArmBots(); + product.eventDatas.forEach(events => { + if (events.type === 'roboticArm') { + addArmBot(selectedProduct.productId, events); + } + }); + } + } + }, [selectedProduct, products]); useEffect(() => { - - }, [armBots]); + }, [armBots]) + + useEffect(() => { + + }, [selectedEventData, selectedEventSphere, isPlaying]); return ( <> + {selectedEventSphere && selectedEventData?.data.type === "roboticArm" && + < ArmBotUI /> + } ); } diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx index 5ca0ec5..efacd53 100644 --- a/app/src/modules/simulation/simulation.tsx +++ b/app/src/modules/simulation/simulation.tsx @@ -23,20 +23,20 @@ function Simulation() { }, [events]) useEffect(() => { - // console.log('products: ', products); + console.log('products: ', products); }, [products]) return ( <> + + {activeModule === 'simulation' && <> - - diff --git a/app/src/modules/simulation/triggers/connector/triggerConnector.tsx b/app/src/modules/simulation/triggers/connector/triggerConnector.tsx index efefa0c..f9fc30f 100644 --- a/app/src/modules/simulation/triggers/connector/triggerConnector.tsx +++ b/app/src/modules/simulation/triggers/connector/triggerConnector.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { useThree } from "@react-three/fiber"; +import { useFrame, useThree } from "@react-three/fiber"; import * as THREE from "three"; import { useSubModuleStore } from "../../../../store/useModuleStore"; import { useSelectedAsset } from "../../../../store/simulation/useSimulationStore"; @@ -7,21 +7,28 @@ import { useProductStore } from "../../../../store/simulation/useProductStore"; import { useEventsStore } from "../../../../store/simulation/useEventsStore"; import { useSelectedProduct } from "../../../../store/simulation/useSimulationStore"; import { handleAddEventToProduct } from "../../events/points/functions/handleAddEventToProduct"; +import { QuadraticBezierLine } from "@react-three/drei"; +import { upsertProductOrEventApi } from "../../../../services/simulation/UpsertProductOrEventApi"; +import { useDeleteTool } from "../../../../store/store"; interface ConnectionLine { id: string; - start: THREE.Vector3; - end: THREE.Vector3; - mid: THREE.Vector3; + startPointUuid: string; + endPointUuid: string; trigger: TriggerSchema; } function TriggerConnector() { - const { gl, raycaster, scene } = useThree(); + const { gl, raycaster, scene, pointer, camera } = useThree(); const { subModule } = useSubModuleStore(); - const { products, getPointByUuid, getIsEventInProduct, getActionByUuid, addTrigger, addEvent, getEventByModelUuid } = useProductStore(); + const { products, getPointByUuid, getIsEventInProduct, getActionByUuid, addTrigger, removeTrigger, addEvent, getEventByModelUuid, getProductById } = useProductStore(); const { selectedAsset, clearSelectedAsset } = useSelectedAsset(); const { selectedProduct } = useSelectedProduct(); + const [hoveredLineKey, setHoveredLineKey] = useState(null); + const groupRefs = useRef>({}); + const [helperlineColor, setHelperLineColor] = useState("red"); + const [currentLine, setCurrentLine] = useState<{ start: THREE.Vector3; end: THREE.Vector3; mid: THREE.Vector3; } | null>(null); + const { deleteTool } = useDeleteTool(); const [firstSelectedPoint, setFirstSelectedPoint] = useState<{ productId: string; @@ -32,52 +39,99 @@ function TriggerConnector() { const [connections, setConnections] = useState([]); + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData + }) + } + useEffect(() => { const newConnections: ConnectionLine[] = []; - products.forEach(product => { - product.eventDatas.forEach(event => { - if ('points' in event) { - event.points.forEach(point => { - if ('action' in point && point.action?.triggers) { - point.action.triggers.forEach(trigger => { - if (trigger.triggeredAsset) { - const targetPoint = getPointByUuid( - product.productId, - trigger.triggeredAsset.triggeredModel.modelUuid, - trigger.triggeredAsset.triggeredPoint.pointUuid - ); + const product = getProductById(selectedProduct.productId); + if (!product || products.length === 0) return; - if (targetPoint) { - const startPos = new THREE.Vector3(...point.position); - const endPos = new THREE.Vector3(...targetPoint.position); - const midPos = new THREE.Vector3() - .addVectors(startPos, endPos) - .multiplyScalar(0.5) - .add(new THREE.Vector3(0, 2, 0)); - - newConnections.push({ - id: `${point.uuid}-${targetPoint.uuid}-${trigger.triggerUuid}`, - start: startPos, - end: endPos, - mid: midPos, - trigger - }); - } - } + product.eventDatas.forEach(event => { + // Handle Conveyor points + if (event.type === "transfer" && 'points' in event) { + event.points.forEach(point => { + if (point.action?.triggers) { + point.action.triggers.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + } + }); + } + // Handle Vehicle point + else if (event.type === "vehicle" && 'point' in event) { + const point = event.point; + if (point.action?.triggers) { + point.action.triggers.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger }); } }); } - }); + } + // Handle Robotic Arm points + else if (event.type === "roboticArm" && 'point' in event) { + const point = event.point; + point.actions?.forEach(action => { + action.triggers?.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + }); + } + // Handle Machine point + else if (event.type === "machine" && 'point' in event) { + const point = event.point; + if (point.action?.triggers) { + point.action.triggers.forEach(trigger => { + if (trigger.triggeredAsset && trigger.triggeredAsset.triggeredPoint) { + newConnections.push({ + id: `${point.uuid}-${trigger.triggeredAsset.triggeredPoint.pointUuid}-${trigger.triggerUuid}`, + startPointUuid: point.uuid, + endPointUuid: trigger.triggeredAsset.triggeredPoint.pointUuid, + trigger + }); + } + }); + } + } }); setConnections(newConnections); - }, [products]); - - useEffect(() => { - console.log('connections: ', connections); - }, connections) + }, [products, selectedProduct.productId]); useEffect(() => { const canvasElement = gl.domElement; @@ -111,15 +165,31 @@ function TriggerConnector() { if (drag) return; evt.preventDefault(); - const intersects = raycaster.intersectObjects(scene.children, true); - if (intersects.length === 0) return; + const intersects = raycaster + .intersectObjects(scene.children, true) + .filter( + (intersect) => + intersect.object.name === ('Event-Sphere') + ); + if (intersects.length === 0) { + setFirstSelectedPoint(null); + return; + }; const currentObject = intersects[0].object; - if (!currentObject || currentObject.name !== 'Event-Sphere') return; + if (!currentObject || currentObject.name !== 'Event-Sphere') { + setFirstSelectedPoint(null); + return; + }; const modelUuid = currentObject.userData.modelUuid; const pointUuid = currentObject.userData.pointUuid; + if (firstSelectedPoint && firstSelectedPoint.pointUuid === pointUuid) { + setFirstSelectedPoint(null); + return; + } + if (selectedProduct && getIsEventInProduct(selectedProduct.productId, modelUuid)) { const point = getPointByUuid( @@ -128,7 +198,12 @@ function TriggerConnector() { pointUuid ); - if (!point) return; + const event = getEventByModelUuid(selectedProduct.productId, modelUuid); + + if (!point || !event) { + setFirstSelectedPoint(null); + return; + }; let actionUuid: string | undefined; if ('action' in point && point.action) { @@ -152,12 +227,12 @@ function TriggerConnector() { delay: 0, triggeredAsset: { triggeredModel: { - modelName: currentObject.parent?.parent?.name || 'Unknown', + modelName: event.modelName || 'Unknown', modelUuid: modelUuid }, triggeredPoint: { - pointName: currentObject.name, - pointUuid: pointUuid + pointName: 'Point', + pointUuid: point.uuid }, triggeredAction: actionUuid ? { actionName: getActionByUuid(selectedProduct.productId, actionUuid)?.actionName || 'Action', @@ -167,7 +242,16 @@ function TriggerConnector() { }; if (firstSelectedPoint.actionUuid) { - addTrigger(firstSelectedPoint.actionUuid, trigger); + const event = addTrigger(selectedProduct.productId, firstSelectedPoint.actionUuid, trigger); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } } setFirstSelectedPoint(null); } @@ -184,7 +268,12 @@ function TriggerConnector() { pointUuid ); - if (!point) return; + const event = getEventByModelUuid(selectedProduct.productId, modelUuid); + + if (!point || !event) { + setFirstSelectedPoint(null); + return; + }; let actionUuid: string | undefined; if ('action' in point && point.action) { @@ -200,12 +289,12 @@ function TriggerConnector() { delay: 0, triggeredAsset: { triggeredModel: { - modelName: currentObject.parent?.parent?.name || 'Unknown', + modelName: event.modelName || 'Unknown', modelUuid: modelUuid }, triggeredPoint: { - pointName: currentObject.name, - pointUuid: pointUuid + pointName: 'Point', + pointUuid: point.uuid }, triggeredAction: actionUuid ? { actionName: getActionByUuid(selectedProduct.productId, actionUuid)?.actionName || 'Action', @@ -215,13 +304,24 @@ function TriggerConnector() { }; if (firstSelectedPoint.actionUuid) { - addTrigger(firstSelectedPoint.actionUuid, trigger); + const event = addTrigger(selectedProduct.productId, firstSelectedPoint.actionUuid, trigger); + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } } setFirstSelectedPoint(null); + } else if (firstSelectedPoint) { + setFirstSelectedPoint(null); } }; - if (subModule === 'simulations') { + if (subModule === 'simulations' && !deleteTool) { canvasElement.addEventListener("mousedown", onMouseDown); canvasElement.addEventListener("mouseup", onMouseUp); canvasElement.addEventListener("mousemove", onMouseMove); @@ -235,11 +335,149 @@ function TriggerConnector() { canvasElement.removeEventListener('contextmenu', handleRightClick); }; - }, [gl, subModule, selectedProduct, firstSelectedPoint]); + }, [gl, subModule, selectedProduct, firstSelectedPoint, deleteTool]); + + + useFrame(() => { + if (firstSelectedPoint) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(scene.children, true).filter( + (intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("agv-collider") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.parent?.name.includes("Zone") && + !(intersect.object.type === "GridHelper") && + !(intersect.object.type === "Line2") + ); + + let point: THREE.Vector3 | null = null; + + if (intersects.length > 0) { + point = intersects[0].point; + if (point.y < 0.05) { + point = new THREE.Vector3(point.x, 0.05, point.z); + } + } + + const sphereIntersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.name === ('Event-Sphere')); + + if (sphereIntersects.length > 0 && sphereIntersects[0].object.uuid === firstSelectedPoint.pointUuid) { + setHelperLineColor('red'); + setCurrentLine(null); + return; + } + + const startPoint = getWorldPositionFromScene(firstSelectedPoint.pointUuid); + + if (point && startPoint) { + if (sphereIntersects.length > 0) { + point = sphereIntersects[0].object.getWorldPosition(new THREE.Vector3()); + } + const distance = startPoint.distanceTo(point); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (startPoint.x + point.x) / 2, + Math.max(startPoint.y, point.y) + heightFactor, + (startPoint.z + point.z) / 2 + ); + + const endPoint = point; + + setCurrentLine({ + start: startPoint, + mid: midPoint, + end: endPoint, + }); + + setHelperLineColor(sphereIntersects.length > 0 ? "#6cf542" : "red"); + } else { + setCurrentLine(null); + } + } else { + setCurrentLine(null); + } + + }) + + const getWorldPositionFromScene = (pointUuid: string): THREE.Vector3 | null => { + const pointObj = scene.getObjectByProperty("uuid", pointUuid); + if (!pointObj) return null; + + const worldPosition = new THREE.Vector3(); + pointObj.getWorldPosition(worldPosition); + return worldPosition; + }; + + const removeConnection = (connection: ConnectionLine) => { + if (connection.trigger.triggerUuid) { + const event = removeTrigger(selectedProduct.productId, connection.trigger.triggerUuid); + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } + } + }; return ( - <> - + + {connections.map((connection) => { + const startPoint = getWorldPositionFromScene(connection.startPointUuid); + const endPoint = getWorldPositionFromScene(connection.endPointUuid); + + if (!startPoint || !endPoint) return null; + + const distance = startPoint.distanceTo(endPoint); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (startPoint.x + endPoint.x) / 2, + Math.max(startPoint.y, endPoint.y) + heightFactor, + (startPoint.z + endPoint.z) / 2 + ); + + return ( + (groupRefs.current[connection.id] = el!)} + start={startPoint.toArray()} + end={endPoint.toArray()} + mid={midPoint.toArray()} + color={deleteTool && hoveredLineKey === connection.id ? "red" : "#42a5f5"} + lineWidth={4} + dashed={deleteTool && hoveredLineKey === connection.id ? false : true} + dashSize={0.75} + dashScale={20} + onPointerOver={() => setHoveredLineKey(connection.id)} + onPointerOut={() => setHoveredLineKey(null)} + onClick={() => { + if (deleteTool) { + setHoveredLineKey(null); + setCurrentLine(null); + removeConnection(connection); + } + }} + userData={connection.trigger} + /> + ); + })} + + {currentLine && ( + + )} + ); } diff --git a/app/src/modules/simulation/ui/arm/PickDropPoints.tsx b/app/src/modules/simulation/ui/arm/PickDropPoints.tsx index 4544a46..c44eee8 100644 --- a/app/src/modules/simulation/ui/arm/PickDropPoints.tsx +++ b/app/src/modules/simulation/ui/arm/PickDropPoints.tsx @@ -9,7 +9,7 @@ interface PickDropProps { actionType: "pick" | "drop"; actionUuid: string; gltfScene: THREE.Group; - selectedPoint: THREE.Mesh | null; + handlePointerDown: (e: ThreeEvent) => void; isSelected: boolean; } @@ -21,12 +21,10 @@ const PickDropPoints: React.FC = ({ actionType, actionUuid, gltfScene, - selectedPoint, handlePointerDown, isSelected, }) => { const groupRef = useRef(null); - return ( = ({ : new THREE.Vector3(0, 0, 0) } onPointerDown={(e) => { - e.stopPropagation(); // Important to prevent event bubbling + + e.stopPropagation(); // Prevent event bubbling if (!isSelected) return; handlePointerDown(e); }} userData={{ modelUuid, pointUuid, actionType, actionUuid }} > { + const cloned = gltfScene.clone(); + cloned.traverse((child: any) => { + if (child.isMesh) { + child.userData = { modelUuid, pointUuid, actionType, actionUuid }; + } + }); + return cloned; + })()} + position={[0, 0, 0]} scale={[0.5, 0.5, 0.5]} /> diff --git a/app/src/modules/simulation/ui/arm/armBotUI.tsx b/app/src/modules/simulation/ui/arm/armBotUI.tsx new file mode 100644 index 0000000..8a2d18f --- /dev/null +++ b/app/src/modules/simulation/ui/arm/armBotUI.tsx @@ -0,0 +1,190 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useSelectedAction, useSelectedEventData, useSelectedEventSphere, useSelectedProduct } from '../../../../store/simulation/useSimulationStore'; +import { useArmBotStore } from '../../../../store/simulation/useArmBotStore'; +import { useGLTF } from '@react-three/drei'; +import { useThree } from '@react-three/fiber'; +import { useProductStore } from '../../../../store/simulation/useProductStore'; +import PickDropPoints from './PickDropPoints'; +import useDraggableGLTF from './useDraggableGLTF'; +import * as THREE from 'three'; + +import armPick from "../../../../assets/gltf-glb/arm_ui_pick.glb"; +import armDrop from "../../../../assets/gltf-glb/arm_ui_drop.glb"; +import useModuleStore from '../../../../store/useModuleStore'; + +type Positions = { + pick: [number, number, number]; + drop: [number, number, number]; + default: [number, number, number]; +}; + +const ArmBotUI = () => { + const { getEventByModelUuid } = useProductStore(); + const { selectedEventData } = useSelectedEventData(); + const { selectedProduct } = useSelectedProduct(); + const { armBots, updateStartPoint, updateEndPoint } = useArmBotStore(); + const { scene } = useThree(); + const { selectedAction } = useSelectedAction(); + + const armUiPick = useGLTF(armPick) as any; + const armUiDrop = useGLTF(armDrop) as any; + + const [startPosition, setStartPosition] = useState<[number, number, number]>([0, 0, 0]); + const [endPosition, setEndPosition] = useState<[number, number, number]>([0, 0, 0]); + const [selectedArmBotData, setSelectedArmBotData] = useState(null); + + // Fetch and setup selected ArmBot data + useEffect(() => { + if (selectedEventData?.data.type === "roboticArm") { + const selectedArmBot = getEventByModelUuid(selectedProduct.productId, selectedEventData.data.modelUuid); + + if (selectedArmBot?.type === "roboticArm") { + setSelectedArmBotData(selectedArmBot); + const defaultPositions = getDefaultPositions(selectedArmBot.modelUuid); + const matchingAction = armBots?.flatMap((robot: ArmBotStatus) => robot.point.actions) + .find((action) => action.actionUuid === selectedAction.actionId); + if (matchingAction) { + const startPoint = matchingAction.process.startPoint; + const pickPosition = (!startPoint || (Array.isArray(startPoint) && startPoint.every(v => v === 0))) + ? defaultPositions.pick + : startPoint; + + const endPoint = matchingAction.process.endPoint; + const dropPosition = (!endPoint || (Array.isArray(endPoint) && endPoint.every(v => v === 0))) + ? defaultPositions.drop + : endPoint; + + setStartPosition(pickPosition); + setEndPosition(dropPosition); + } + } + } + }, [selectedEventData, selectedProduct, getEventByModelUuid, selectedAction]); + + + function getDefaultPositions(modelUuid: string): Positions { + const modelData = getEventByModelUuid(selectedProduct.productId, modelUuid); + + if (modelData?.type === "roboticArm") { + const baseX = modelData.point.position?.[0] || 0; + const baseY = modelData.point.position?.[1] || 0;; + const baseZ = modelData.point.position?.[2] || 0; + return { + pick: [baseX, baseY, baseZ + 0.5], + drop: [baseX, baseY, baseZ - 0.5], + default: [baseX, baseY, baseZ], + }; + } + + return { + pick: [0.5, 1.5, 0], + drop: [-0.5, 1.5, 0], + default: [0, 1.5, 0], + }; + } + + function getLocalPosition(parentUuid: string, worldPosArray: [number, number, number] | null): [number, number, number] | null { + if (worldPosArray) { + const worldPos = new THREE.Vector3(...worldPosArray); + const parentObject = scene.getObjectByProperty('uuid', parentUuid); + + if (parentObject) { + const localPos = worldPos.clone(); + parentObject.worldToLocal(localPos); + return [localPos.x, localPos.y, localPos.z]; + } + } + return null; + } + + const updatePointToState = (obj: THREE.Object3D) => { + const { modelUuid, actionType, actionUuid } = obj.userData; + const newPosition = new THREE.Vector3(); + obj.getWorldPosition(newPosition); + const worldPositionArray = newPosition.toArray() as [number, number, number]; + + if (selectedEventData?.data.type === "roboticArm") { + const selectedArmBot = getEventByModelUuid(selectedProduct.productId, selectedEventData.data.modelUuid); + + const armBot = selectedArmBot?.modelUuid === modelUuid ? selectedArmBot : null; + if (!armBot) return; + + if (armBot.type === "roboticArm") { + armBot?.point?.actions?.map((action: any) => { + if (action.actionUuid === actionUuid) { + const updatedProcess = { ...action.process }; + + if (actionType === "pick") { + updatedProcess.startPoint = getLocalPosition(modelUuid, worldPositionArray); + setStartPosition(updatedProcess.startPoint) + updateStartPoint(modelUuid, actionUuid, updatedProcess.startPoint); + } else if (actionType === "drop") { + updatedProcess.endPoint = getLocalPosition(modelUuid, worldPositionArray); + setEndPosition(updatedProcess.endPoint) + updateEndPoint(modelUuid, actionUuid, updatedProcess.endPoint); + } + return { + ...action, + process: updatedProcess, + }; + } + return action; + }); + + } + } + } + + useEffect(() => { + + + }, [armBots]) + + const { handlePointerDown } = useDraggableGLTF(updatePointToState); + + if (!selectedArmBotData || !Array.isArray(selectedArmBotData.point?.actions)) { + return null; // avoid rendering if no data yet + } + return ( + <> + {selectedArmBotData.point.actions.map((action: any) => { + if (action.actionUuid === selectedAction.actionId) { + return ( + + + + + + + ); + } else { + return null; // important! must return something + } + })} + + ); + +}; + +export default ArmBotUI; diff --git a/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts b/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts index b7e9272..e450bfd 100644 --- a/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts +++ b/app/src/modules/simulation/ui/arm/useDraggableGLTF.ts @@ -1,11 +1,11 @@ -import { useRef } from "react"; +import { useRef, useState } from "react"; import * as THREE from "three"; import { ThreeEvent, useThree } from "@react-three/fiber"; type OnUpdateCallback = (object: THREE.Object3D) => void; export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { - const { camera, gl, controls, scene } = useThree(); + const { camera, gl, controls } = useThree(); const activeObjRef = useRef(null); const planeRef = useRef( new THREE.Plane(new THREE.Vector3(0, 1, 0), 0) @@ -15,6 +15,7 @@ export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(); + const [objectWorldPos, setObjectWorldPos] = useState(new THREE.Vector3()); const handlePointerDown = (e: ThreeEvent) => { e.stopPropagation(); @@ -33,10 +34,10 @@ export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { activeObjRef.current = obj; initialPositionRef.current.copy(obj.position); + // Get world position - const objectWorldPos = new THREE.Vector3(); - obj.getWorldPosition(objectWorldPos); + setObjectWorldPos(obj.getWorldPosition(objectWorldPos)); // Set plane at the object's Y level planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y); @@ -61,52 +62,56 @@ export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { const handlePointerMove = (e: PointerEvent) => { if (!activeObjRef.current) return; - + // Check if Shift key is pressed const isShiftKeyPressed = e.shiftKey; - + // Get the mouse position relative to the canvas const rect = gl.domElement.getBoundingClientRect(); pointer.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; pointer.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; - + // Update raycaster to point to the mouse position raycaster.setFromCamera(pointer, camera); - + // Create a vector to store intersection point const intersection = new THREE.Vector3(); - const intersects = raycaster.ray.intersectPlane(planeRef.current, intersection); + const intersects = raycaster.ray.intersectPlane( + planeRef.current, + intersection + ); if (!intersects) return; - + // Add offset for dragging intersection.add(offsetRef.current); - console.log('intersection: ', intersection); - + // Get the parent's world matrix if exists const parent = activeObjRef.current.parent; const targetPosition = new THREE.Vector3(); - + + // OnPointerDown + initialPositionRef.current.copy(objectWorldPos); + + // OnPointerMove if (isShiftKeyPressed) { - console.log('isShiftKeyPressed: ', isShiftKeyPressed); - // For Y-axis only movement, maintain original X and Z - console.log('initialPositionRef: ', initialPositionRef); - console.log('intersection.y: ', intersection); - targetPosition.set( - initialPositionRef.current.x, - intersection.y, - initialPositionRef.current.z - ); + const { x: initialX, y: initialY } = initialPositionRef.current; + const { x: objectX, z: objectZ } = objectWorldPos; + + const deltaX = intersection.x - initialX; + + targetPosition.set(objectX, initialY + deltaX, objectZ); } else { // For free movement targetPosition.copy(intersection); } - + // Convert world position to local if object is nested inside a parent if (parent) { parent.worldToLocal(targetPosition); } - + // Update object position + activeObjRef.current.position.copy(targetPosition); }; @@ -126,6 +131,3 @@ export default function useDraggableGLTF(onUpdate: OnUpdateCallback) { return { handlePointerDown }; } - - - diff --git a/app/src/modules/simulation/ui/vehicle/vehicleUI.tsx b/app/src/modules/simulation/ui/vehicle/vehicleUI.tsx index 5dec724..2a124cd 100644 --- a/app/src/modules/simulation/ui/vehicle/vehicleUI.tsx +++ b/app/src/modules/simulation/ui/vehicle/vehicleUI.tsx @@ -1,12 +1,16 @@ import React, { useEffect, useRef, useState } from 'react'; +import * as Types from "../../../../types/world/worldTypes"; import startPoint from "../../../../assets/gltf-glb/arrow_green.glb"; -import startEnd from "../../../../assets/gltf-glb/arrow_red.glb"; import * as THREE from "three"; +import startEnd from "../../../../assets/gltf-glb/arrow_red.glb"; import { useGLTF } from '@react-three/drei'; import { useFrame, useThree } from '@react-three/fiber'; -import { useSelectedEventSphere } from '../../../../store/simulation/useSimulationStore'; +import { useSelectedEventSphere, useIsDragging, useIsRotating } from '../../../../store/simulation/useSimulationStore'; import { useVehicleStore } from '../../../../store/simulation/useVehicleStore'; -import * as Types from "../../../../types/world/worldTypes"; +import { useProductStore } from '../../../../store/simulation/useProductStore'; +import { useSelectedProduct } from '../../../../store/simulation/useSimulationStore'; +import { upsertProductOrEventApi } from '../../../../services/simulation/UpsertProductOrEventApi'; + const VehicleUI = () => { const { scene: startScene } = useGLTF(startPoint) as any; const { scene: endScene } = useGLTF(startEnd) as any; @@ -14,67 +18,60 @@ const VehicleUI = () => { const endMarker = useRef(null); const prevMousePos = useRef({ x: 0, y: 0 }); const { selectedEventSphere } = useSelectedEventSphere(); - const { vehicles, updateVehicle } = useVehicleStore(); + const { selectedProduct } = useSelectedProduct(); + const { getVehicleById } = useVehicleStore(); + const { updateEvent } = useProductStore(); const [startPosition, setStartPosition] = useState<[number, number, number]>([0, 0, 0]); const [endPosition, setEndPosition] = useState<[number, number, number]>([0, 0, 0]); const [startRotation, setStartRotation] = useState<[number, number, number]>([0, 0, 0]); const [endRotation, setEndRotation] = useState<[number, number, number]>([0, 0, 0]); - const [isDragging, setIsDragging] = useState<"start" | "end" | null>(null); - const [isRotating, setIsRotating] = useState<"start" | "end" | null>(null); + const { isDragging, setIsDragging } = useIsDragging(); + const { isRotating, setIsRotating } = useIsRotating(); const { raycaster } = useThree(); const plane = useRef(new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)); const state: Types.ThreeState = useThree(); const controls: any = state.controls; + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const updateBackend = ( + productName: string, + productId: string, + organization: string, + eventData: EventsSchema + ) => { + upsertProductOrEventApi({ + productName: productName, + productId: productId, + organization: organization, + eventDatas: eventData + }) + } + useEffect(() => { if (!selectedEventSphere) return; - const selectedVehicle = vehicles.find( - (vehicle: any) => vehicle.modelUuid === selectedEventSphere.userData.modelUuid - ); + const selectedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid); if (selectedVehicle?.point?.action) { const { pickUpPoint, unLoadPoint } = selectedVehicle.point.action; if (pickUpPoint) { - const pickupPosition = new THREE.Vector3( - pickUpPoint.position.x, - pickUpPoint.position.y, - pickUpPoint.position.z - ); - const pickupRotation = new THREE.Vector3( - pickUpPoint.rotation.x, - pickUpPoint.rotation.y, - pickUpPoint.rotation.z - ); - pickupPosition.y = 0; - setStartPosition([pickupPosition.x, 0, pickupPosition.z]); - setStartRotation([pickupRotation.x, pickupRotation.y, pickupRotation.z]); + setStartPosition([pickUpPoint.position.x, 0, pickUpPoint.position.z]); + setStartRotation([pickUpPoint.rotation.x, pickUpPoint.rotation.y, pickUpPoint.rotation.z]); } else { const defaultLocal = new THREE.Vector3(0, 0, 1.5); const defaultWorld = selectedEventSphere.localToWorld(defaultLocal); - defaultWorld.y = 0; setStartPosition([defaultWorld.x, 0, defaultWorld.z]); setStartRotation([0, 0, 0]); } if (unLoadPoint) { - const unLoadPosition = new THREE.Vector3( - unLoadPoint.position.x, - unLoadPoint.position.y, - unLoadPoint.position.z - ); - const unLoadRotation = new THREE.Vector3( - unLoadPoint.rotation.x, - unLoadPoint.rotation.y, - unLoadPoint.position.z - ); - unLoadPosition.y = 0; // Force y to 0 - setEndPosition([unLoadPosition.x, 0, unLoadPosition.z]); - setEndRotation([unLoadRotation.x, unLoadRotation.y, unLoadRotation.z]); + setEndPosition([unLoadPoint.position.x, 0, unLoadPoint.position.z]); + setEndRotation([unLoadPoint.rotation.x, unLoadPoint.rotation.y, unLoadPoint.rotation.z]); } else { const defaultLocal = new THREE.Vector3(0, 0, -1.5); const defaultWorld = selectedEventSphere.localToWorld(defaultLocal); - defaultWorld.y = 0; // Force y to 0 setEndPosition([defaultWorld.x, 0, defaultWorld.z]); setEndRotation([0, 0, 0]); } @@ -87,7 +84,6 @@ const VehicleUI = () => { const intersects = raycaster.ray.intersectPlane(plane.current, intersectPoint); if (intersects) { - intersectPoint.y = 0; // Force y to 0 if (isDragging === "start") { setStartPosition([intersectPoint.x, 0, intersectPoint.z]); } @@ -108,12 +104,12 @@ const VehicleUI = () => { if (marker) { const rotationSpeed = 10; - marker.rotation.y += deltaX * rotationSpeed; if (isRotating === 'start') { - setStartRotation([marker.rotation.x, marker.rotation.y, marker.rotation.z]) + const y = startRotation[1] + deltaX * rotationSpeed; + setStartRotation([0, y, 0]); } else { - - setEndRotation([marker.rotation.x, marker.rotation.y, marker.rotation.z]) + const y = endRotation[1] + deltaX * rotationSpeed; + setEndRotation([0, y, 0]); } } }); @@ -142,43 +138,34 @@ const VehicleUI = () => { setIsRotating(null); if (selectedEventSphere?.userData.modelUuid) { - const updatedVehicle = vehicles.find( - (vehicle) => vehicle.modelUuid === selectedEventSphere.userData.modelUuid - ); + const updatedVehicle = getVehicleById(selectedEventSphere.userData.modelUuid); if (updatedVehicle) { - updateVehicle(selectedEventSphere.userData.modelUuid, { + const event = updateEvent(selectedProduct.productId, selectedEventSphere.userData.modelUuid, { point: { ...updatedVehicle.point, action: { ...updatedVehicle.point?.action, pickUpPoint: { - position: { - x: startPosition[0], - y: startPosition[1], - z: startPosition[2], - }, - rotation: { - x: startRotation[0], - y: startRotation[1], - z: startRotation[2], - }, + position: { x: startPosition[0], y: startPosition[1], z: startPosition[2], }, + rotation: { x: 0, y: startRotation[1], z: 0, }, }, unLoadPoint: { - position: { - x: endPosition[0], - y: endPosition[1], - z: endPosition[2], - }, - rotation: { - x: endRotation[0], - y: endRotation[1], - z: endRotation[2], - }, + position: { x: endPosition[0], y: endPosition[1], z: endPosition[2], }, + rotation: { x: 0, y: endRotation[1], z: 0, }, }, }, }, - }); + }) + + if (event) { + updateBackend( + selectedProduct.productName, + selectedProduct.productId, + organization, + event + ); + } } } }; @@ -208,6 +195,7 @@ const VehicleUI = () => { object={startScene} ref={startMarker} position={startPosition} + rotation={startRotation} onPointerDown={(e: any) => { e.stopPropagation(); handlePointerDown(e, "start", "start"); @@ -224,6 +212,7 @@ const VehicleUI = () => { object={endScene} ref={endMarker} position={endPosition} + rotation={endRotation} onPointerDown={(e: any) => { e.stopPropagation(); handlePointerDown(e, "end", "end"); diff --git a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx index 2f0b235..aeadc4b 100644 --- a/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx +++ b/app/src/modules/simulation/vehicle/instances/animator/vehicleAnimator.tsx @@ -11,7 +11,7 @@ interface VehicleAnimatorProps { reset: () => void; currentPhase: string; agvUuid: number; - agvDetail: any; + agvDetail: VehicleStatus; } function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetail, reset }: VehicleAnimatorProps) { @@ -25,14 +25,14 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai const completedRef = useRef(false); const isPausedRef = useRef(false); const pauseTimeRef = useRef(null); + const [progress, setProgress] = useState(0); const [restRotation, setRestingRotation] = useState(true); const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]); - const [progress, setProgress] = useState(0); const { scene } = useThree(); let startTime: number; let fixedInterval: number; let coveredDistance = progressRef.current; - let objectRotation = (agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 }) as { x: number; y: number; z: number }; + let objectRotation = (agvDetail.point?.action?.pickUpPoint?.rotation || { x: 0, y: 0, z: 0 }) as { x: number; y: number; z: number } | undefined; useEffect(() => { @@ -66,11 +66,12 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai setReset(false); setRestingRotation(true); decrementVehicleLoad(agvDetail.modelUuid, 0); + isPausedRef.current = false; + pauseTimeRef.current = 0; const object = scene.getObjectByProperty('uuid', agvUuid); - console.log('currentPhase: ', currentPhase); if (object) { object.position.set(agvDetail.position[0], agvDetail.position[1], agvDetail.position[2]); - object.rotation.set(objectRotation.x, objectRotation.y, objectRotation.z); + object.rotation.set(agvDetail.rotation[0], agvDetail.rotation[1], agvDetail.rotation[2]); } } }, [isReset, isPlaying]) @@ -132,7 +133,7 @@ function VehicleAnimator({ path, handleCallBack, currentPhase, agvUuid, agvDetai } if (progressRef.current >= totalDistance) { - if (restRotation) { + if (restRotation && objectRotation) { const targetQuaternion = new THREE.Quaternion().setFromEuler(new THREE.Euler(objectRotation.x, objectRotation.y, objectRotation.z)); object.quaternion.slerp(targetQuaternion, delta * 2); const angleDiff = object.quaternion.angleTo(targetQuaternion); diff --git a/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx b/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx index 6a81d3a..412a4ba 100644 --- a/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx +++ b/app/src/modules/simulation/vehicle/instances/instance/vehicleInstance.tsx @@ -30,7 +30,8 @@ function VehicleInstance({ agvDetail }: any) { ); function vehicleStatus(modelId: string, status: string) { - // console.log(`${modelId} , ${status}); + // console.log(`${modelId} , ${status}`); + } // Function to reset everything @@ -44,7 +45,7 @@ function VehicleInstance({ agvDetail }: any) { const increment = () => { if (isIncrememtable.current) { - incrementVehicleLoad(agvDetail.modelUuid, 2); + incrementVehicleLoad(agvDetail.modelUuid, 10); isIncrememtable.current = false; } } @@ -69,6 +70,7 @@ function VehicleInstance({ agvDetail }: any) { increment(); }, 5000); + if (agvDetail.currentLoad === agvDetail.point.action.loadCapacity) { const toDrop = computePath( agvDetail.point.action.pickUpPoint.position, diff --git a/app/src/modules/simulation/vehicle/instances/vehicleInstances.tsx b/app/src/modules/simulation/vehicle/instances/vehicleInstances.tsx index 91111cf..fcc840d 100644 --- a/app/src/modules/simulation/vehicle/instances/vehicleInstances.tsx +++ b/app/src/modules/simulation/vehicle/instances/vehicleInstances.tsx @@ -9,10 +9,8 @@ function VehicleInstances() { return ( <> - {vehicles.map((val: any, i: any) => - - - + {vehicles.map((val: VehicleStatus) => + )} diff --git a/app/src/modules/simulation/vehicle/vehicles.tsx b/app/src/modules/simulation/vehicle/vehicles.tsx index 7badec5..3fbc8ef 100644 --- a/app/src/modules/simulation/vehicle/vehicles.tsx +++ b/app/src/modules/simulation/vehicle/vehicles.tsx @@ -1,220 +1,48 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import VehicleInstances from "./instances/vehicleInstances"; import { useVehicleStore } from "../../../store/simulation/useVehicleStore"; -import { useFloorItems } from "../../../store/store"; -import { useSelectedEventData, useSelectedEventSphere } from "../../../store/simulation/useSimulationStore"; +import { useSelectedEventData, useSelectedEventSphere, useSelectedProduct } from "../../../store/simulation/useSimulationStore"; import VehicleUI from "../ui/vehicle/vehicleUI"; import { usePlayButtonStore } from "../../../store/usePlayButtonStore"; -function Vehicles() { +import { useProductStore } from "../../../store/simulation/useProductStore"; - const { vehicles, addVehicle } = useVehicleStore(); +function Vehicles() { + const { products, getProductById } = useProductStore(); + const { selectedProduct } = useSelectedProduct(); + const { vehicles, addVehicle, clearvehicles } = useVehicleStore(); const { selectedEventSphere } = useSelectedEventSphere(); const { selectedEventData } = useSelectedEventData(); - const { floorItems } = useFloorItems(); const { isPlaying } = usePlayButtonStore(); - const [vehicleStatusSample, setVehicleStatusSample] = useState< - VehicleEventSchema[] - >([ - { - modelUuid: "9356f710-4727-4b50-bdb2-9c1e747ecc74", - modelName: "AGV", - position: [97.9252965204558, 0, 37.96138815638661], - rotation: [0, 0, 0], - state: "idle", - type: "vehicle", - speed: 2.5, - point: { - uuid: "point-789", - position: [0, 1, 0], - rotation: [0, 0, 0], - action: { - actionUuid: "action-456", - actionName: "Deliver to Zone A", - actionType: "travel", - unLoadDuration: 10, - loadCapacity: 2, - steeringAngle:0, - pickUpPoint: { position: { x: 98.71483985219794, y: 0, z: 28.66321267938962 }, rotation: { x: 0, y: 0, z: 0 } }, - unLoadPoint: { position: { x: 105.71483985219794, y: 0, z: 28.66321267938962 }, rotation: { x: 0, y: 0, z: 0 } }, - triggers: [ - { - triggerUuid: "trig-001", - triggerName: "Start Travel", - triggerType: "onComplete", - delay: 0, - triggeredAsset: { - triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" }, - triggeredPoint: { pointName: "Pickup Arm Point", pointUuid: "arm-point-01" }, - triggeredAction: { actionName: "Grab Widget", actionUuid: "grab-001" } - } - }, - { - triggerUuid: "trig-002", - triggerName: "Complete Travel", - triggerType: "onComplete", - delay: 2, - triggeredAsset: null - } - ] - } - } - }, - { - modelUuid: "b06960bb-3d2e-41f7-a646-335f389c68b4", - modelName: "AGV", - position: [89.61609306554463, 0, 33.634136622267356], - rotation: [0, 0, 0], - state: "idle", - type: "vehicle", - speed: 2.5, - point: { - uuid: "point-789", - position: [0, 1, 0], - rotation: [0, 0, 0], - action: { - actionUuid: "action-456", - actionName: "Deliver to Zone A", - actionType: "travel", - unLoadDuration: 10, - loadCapacity: 2, - steeringAngle:0, - pickUpPoint: null, - unLoadPoint: null, - triggers: [ - { - triggerUuid: "trig-001", - triggerName: "Start Travel", - triggerType: "onStart", - delay: 0, - triggeredAsset: { - triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" }, - triggeredPoint: { pointName: "Pickup Arm Point", pointUuid: "arm-point-01" }, - triggeredAction: { actionName: "Grab Widget", actionUuid: "grab-001" } - } - }, - { - triggerUuid: "trig-002", - triggerName: "Complete Travel", - triggerType: "onComplete", - delay: 2, - triggeredAsset: null - } - ] - } - } - }, - // { - // modelUuid: "cd7d0584-0684-42b4-b051-9e882c1914aa", - // modelName: "AGV", - // position: [105.90938758014703, 0, 31.584209911095215], - // rotation: [0, 0, 0], - // state: "idle", - // type: "vehicle", - // speed: 2.5, - // point: { - // uuid: "point-789", - // position: [0, 1, 0], - // rotation: [0, 0, 0], - // action: { - // actionUuid: "action-456", - // actionName: "Deliver to Zone A", - // actionType: "travel", - // unLoadDuration: 10, - // loadCapacity: 2, - // steeringAngle:0, - // pickUpPoint: null, - // unLoadPoint: null, - // triggers: [ - // { - // triggerUuid: "trig-001", - // triggerName: "Start Travel", - // triggerType: "onStart", - // delay: 0, - // triggeredAsset: { - // triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" }, - // triggeredPoint: { pointName: "Pickup Arm Point", pointUuid: "arm-point-01" }, - // triggeredAction: { actionName: "Grab Widget", actionUuid: "grab-001" } - // } - // }, - // { - // triggerUuid: "trig-002", - // triggerName: "Complete Travel", - // triggerType: "onComplete", - // delay: 2, - // triggeredAsset: null - // } - // ] - // } - // } - // }, - // { - // modelUuid: "e729a4f1-11d2-4778-8d6a-468f1b4f6b79", - // modelName: "forklift", - // position: [98.85729337188162, 0, 38.36616546567653], - // rotation: [0, 0, 0], - // state: "idle", - // type: "vehicle", - // speed: 2.5, - // point: { - // uuid: "point-789", - // position: [0, 1, 0], - // rotation: [0, 0, 0], - // action: { - // actionUuid: "action-456", - // actionName: "Deliver to Zone A", - // actionType: "travel", - // unLoadDuration: 15, - // loadCapacity: 5, - // steeringAngle:0, - // pickUpPoint: null, - // unLoadPoint: null, - // triggers: [ - // { - // triggerUuid: "trig-001", - // triggerName: "Start Travel", - // triggerType: "onStart", - // delay: 0, - // triggeredAsset: { - // triggeredModel: { modelName: "ArmBot-X", modelUuid: "arm-001" }, - // triggeredPoint: { pointName: "Pickup Arm Point", pointUuid: "arm-point-01" }, - // triggeredAction: { actionName: "Grab Widget", actionUuid: "grab-001" } - // } - // }, - // { - // triggerUuid: "trig-002", - // triggerName: "Complete Travel", - // triggerType: "onComplete", - // delay: 2, - // triggeredAsset: null - // } - // ] - // } - // } - // } - ]); useEffect(() => { - // console.log('vehicles: ', vehicles); + if (selectedProduct.productId) { + const product = getProductById(selectedProduct.productId); + if (product) { + clearvehicles(); + product.eventDatas.forEach(events => { + if (events.type === 'vehicle') { + addVehicle(selectedProduct.productId, events); + } + }); + } + } + }, [selectedProduct, products]); + + useEffect(() => { + // }, [vehicles]) - useEffect(() => { - addVehicle("123", vehicleStatusSample[0]); - addVehicle('123', vehicleStatusSample[1]); - // addVehicle('123', vehicleStatusSample[2]); - }, []); - - return ( <> + + {selectedEventSphere && selectedEventData?.data.type === "vehicle" && !isPlaying && < VehicleUI /> } + ); } -export default Vehicles; - - - +export default Vehicles; \ No newline at end of file diff --git a/app/src/modules/visualization/RealTimeVisulization.tsx b/app/src/modules/visualization/RealTimeVisulization.tsx index caf37a1..a8f44b4 100644 --- a/app/src/modules/visualization/RealTimeVisulization.tsx +++ b/app/src/modules/visualization/RealTimeVisulization.tsx @@ -66,8 +66,7 @@ const RealTimeVisulization: React.FC = () => { const { selectedZone, setSelectedZone } = useSelectedZoneStore(); const { setRightSelect } = useRightSelected(); - const { editWidgetOptions, setEditWidgetOptions } = - useEditWidgetOptionsStore(); + const { editWidgetOptions, setEditWidgetOptions } = useEditWidgetOptionsStore(); const { rightClickSelected, setRightClickSelected } = useRightClickSelected(); const [openConfirmationPopup, setOpenConfirmationPopup] = useState(false); const { setFloatingWidget } = useFloatingWidget(); @@ -76,8 +75,6 @@ const RealTimeVisulization: React.FC = () => { const { setSelectedChartId } = useWidgetStore(); const [waitingPanels, setWaitingPanels] = useState(null); - console.log("waitingPanels: ", waitingPanels); - OuterClick({ contextClassName: [ "chart-container", @@ -145,109 +142,109 @@ const RealTimeVisulization: React.FC = () => { }); }, [selectedZone]); - const handleDrop = async (event: React.DragEvent) => { - event.preventDefault(); - try { - const email = localStorage.getItem("email") ?? ""; - const organization = email?.split("@")[1]?.split(".")[0]; + // const handleDrop = async (event: React.DragEvent) => { + // event.preventDefault(); + // try { + // const email = localStorage.getItem("email") ?? ""; + // const organization = email?.split("@")[1]?.split(".")[0]; - const data = event.dataTransfer.getData("text/plain"); - if (widgetSubOption === "3D") return; - if (!data || selectedZone.zoneName === "") return; + // const data = event.dataTransfer.getData("text/plain"); + // if (widgetSubOption === "3D") return; + // if (!data || selectedZone.zoneName === "") return; - const droppedData = JSON.parse(data); - const canvasElement = document.getElementById("real-time-vis-canvas"); - if (!canvasElement) throw new Error("Canvas element not found"); + // const droppedData = JSON.parse(data); + // const canvasElement = document.getElementById("real-time-vis-canvas"); + // if (!canvasElement) throw new Error("Canvas element not found"); - const rect = canvasElement.getBoundingClientRect(); - const relativeX = event.clientX - rect.left; - const relativeY = event.clientY - rect.top; + // const rect = canvasElement.getBoundingClientRect(); + // const relativeX = event.clientX - rect.left; + // const relativeY = event.clientY - rect.top; - // Widget dimensions - const widgetWidth = droppedData.width ?? 125; - const widgetHeight = droppedData.height ?? 100; + // // Widget dimensions + // const widgetWidth = droppedData.width ?? 125; + // const widgetHeight = droppedData.height ?? 100; - // Center the widget at cursor - const centerOffsetX = widgetWidth / 2; - const centerOffsetY = widgetHeight / 2; + // // Center the widget at cursor + // const centerOffsetX = widgetWidth / 2; + // const centerOffsetY = widgetHeight / 2; - const adjustedX = relativeX - centerOffsetX; - const adjustedY = relativeY - centerOffsetY; + // const adjustedX = relativeX - centerOffsetX; + // const adjustedY = relativeY - centerOffsetY; - const finalPosition = determinePosition(rect, adjustedX, adjustedY); - const [activeProp1, activeProp2] = getActiveProperties(finalPosition); + // const finalPosition = determinePosition(rect, adjustedX, adjustedY); + // const [activeProp1, activeProp2] = getActiveProperties(finalPosition); - let finalY = 0; - let finalX = 0; + // let finalY = 0; + // let finalX = 0; - if (activeProp1 === "top") { - finalY = adjustedY; - } else { - finalY = rect.height - (adjustedY + widgetHeight); - } + // if (activeProp1 === "top") { + // finalY = adjustedY; + // } else { + // finalY = rect.height - (adjustedY + widgetHeight); + // } - if (activeProp2 === "left") { - finalX = adjustedX; - } else { - finalX = rect.width - (adjustedX + widgetWidth); - } + // if (activeProp2 === "left") { + // finalX = adjustedX; + // } else { + // finalX = rect.width - (adjustedX + widgetWidth); + // } - // Clamp to boundaries - finalX = Math.max(0, Math.min(rect.width - widgetWidth, finalX)); - finalY = Math.max(0, Math.min(rect.height - widgetHeight, finalY)); + // // Clamp to boundaries + // finalX = Math.max(0, Math.min(rect.width - widgetWidth, finalX)); + // finalY = Math.max(0, Math.min(rect.height - widgetHeight, finalY)); - const boundedPosition = { - ...finalPosition, - [activeProp1]: finalY, - [activeProp2]: finalX, - [activeProp1 === "top" ? "bottom" : "top"]: "auto", - [activeProp2 === "left" ? "right" : "left"]: "auto", - }; + // const boundedPosition = { + // ...finalPosition, + // [activeProp1]: finalY, + // [activeProp2]: finalX, + // [activeProp1 === "top" ? "bottom" : "top"]: "auto", + // [activeProp2 === "left" ? "right" : "left"]: "auto", + // }; - const newObject = { - ...droppedData, - id: generateUniqueId(), - position: boundedPosition, - }; + // const newObject = { + // ...droppedData, + // id: generateUniqueId(), + // position: boundedPosition, + // }; - const existingZone = - useDroppedObjectsStore.getState().zones[selectedZone.zoneName]; - if (!existingZone) { - useDroppedObjectsStore - .getState() - .setZone(selectedZone.zoneName, selectedZone.zoneId); - } + // const existingZone = + // useDroppedObjectsStore.getState().zones[selectedZone.zoneName]; + // if (!existingZone) { + // useDroppedObjectsStore + // .getState() + // .setZone(selectedZone.zoneName, selectedZone.zoneId); + // } - const addFloatingWidget = { - organization, - widget: newObject, - zoneId: selectedZone.zoneId, - }; + // const addFloatingWidget = { + // organization, + // widget: newObject, + // zoneId: selectedZone.zoneId, + // }; - if (visualizationSocket) { - visualizationSocket.emit("v2:viz-float:add", addFloatingWidget); - } + // if (visualizationSocket) { + // visualizationSocket.emit("v2:viz-float:add", addFloatingWidget); + // } - useDroppedObjectsStore - .getState() - .addObject(selectedZone.zoneName, newObject); + // useDroppedObjectsStore + // .getState() + // .addObject(selectedZone.zoneName, newObject); - const droppedObjectsStore = useDroppedObjectsStore.getState(); - const currentZone = droppedObjectsStore.zones[selectedZone.zoneName]; + // const droppedObjectsStore = useDroppedObjectsStore.getState(); + // const currentZone = droppedObjectsStore.zones[selectedZone.zoneName]; - if (currentZone && currentZone.zoneId === selectedZone.zoneId) { - console.log( - `Objects for Zone ${selectedZone.zoneId}:`, - currentZone.objects - ); - setFloatingWidget(currentZone.objects); - } else { - console.warn("Zone not found or zoneId mismatch"); - } - } catch (error) { - console.error("Error in handleDrop:", error); - } - }; + // if (currentZone && currentZone.zoneId === selectedZone.zoneId) { + // console.log( + // `Objects for Zone ${selectedZone.zoneId}:`, + // currentZone.objects + // ); + // setFloatingWidget(currentZone.objects); + // } else { + // console.warn("Zone not found or zoneId mismatch"); + // } + // } catch (error) { + // console.error("Error in handleDrop:", error); + // } + // }; useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -304,16 +301,7 @@ const RealTimeVisulization: React.FC = () => { } `} -
+
{openConfirmationPopup && ( @@ -324,20 +312,6 @@ const RealTimeVisulization: React.FC = () => { /> )} -
handleDrop(event)} - onDragOver={(event) => event.preventDefault()} - > - -
{activeModule === "visualization" && selectedZone.zoneName !== "" && ( )} diff --git a/app/src/modules/visualization/functions/handleUiDrop.ts b/app/src/modules/visualization/functions/handleUiDrop.ts new file mode 100644 index 0000000..a70742e --- /dev/null +++ b/app/src/modules/visualization/functions/handleUiDrop.ts @@ -0,0 +1,122 @@ +import { generateUniqueId } from "../../../functions/generateUniqueId"; +import { useDroppedObjectsStore } from "../../../store/visualization/useDroppedObjectsStore"; +import { determinePosition } from "./determinePosition"; +import { getActiveProperties } from "./getActiveProperties"; + +interface HandleDropProps { + widgetSubOption: any; + visualizationSocket: any; + selectedZone: any; + setFloatingWidget: (value: any) => void; + event: React.DragEvent +} + +export const createHandleDrop = ({ + widgetSubOption, + visualizationSocket, + selectedZone, + setFloatingWidget, + event, +}: HandleDropProps) => { + event.preventDefault(); + try { + const email = localStorage.getItem("email") ?? ""; + const organization = email?.split("@")[1]?.split(".")[0]; + + const data = event.dataTransfer.getData("text/plain"); + if (widgetSubOption === "3D") return; + if (!data || selectedZone.zoneName === "") return; + + const droppedData = JSON.parse(data); + const canvasElement = document.getElementById("real-time-vis-canvas"); + if (!canvasElement) throw new Error("Canvas element not found"); + + const rect = canvasElement.getBoundingClientRect(); + const relativeX = event.clientX - rect.left; + const relativeY = event.clientY - rect.top; + + // Widget dimensions + const widgetWidth = droppedData.width ?? 125; + const widgetHeight = droppedData.height ?? 100; + + // Center the widget at cursor + const centerOffsetX = widgetWidth / 2; + const centerOffsetY = widgetHeight / 2; + + const adjustedX = relativeX - centerOffsetX; + const adjustedY = relativeY - centerOffsetY; + + const finalPosition = determinePosition(rect, adjustedX, adjustedY); + const [activeProp1, activeProp2] = getActiveProperties(finalPosition); + + let finalY = 0; + let finalX = 0; + + if (activeProp1 === "top") { + finalY = adjustedY; + } else { + finalY = rect.height - (adjustedY + widgetHeight); + } + + if (activeProp2 === "left") { + finalX = adjustedX; + } else { + finalX = rect.width - (adjustedX + widgetWidth); + } + + // Clamp to boundaries + finalX = Math.max(0, Math.min(rect.width - widgetWidth, finalX)); + finalY = Math.max(0, Math.min(rect.height - widgetHeight, finalY)); + + const boundedPosition = { + ...finalPosition, + [activeProp1]: finalY, + [activeProp2]: finalX, + [activeProp1 === "top" ? "bottom" : "top"]: "auto", + [activeProp2 === "left" ? "right" : "left"]: "auto", + }; + + const newObject = { + ...droppedData, + id: generateUniqueId(), + position: boundedPosition, + }; + + const existingZone = + useDroppedObjectsStore.getState().zones[selectedZone.zoneName]; + if (!existingZone) { + useDroppedObjectsStore + .getState() + .setZone(selectedZone.zoneName, selectedZone.zoneId); + } + + const addFloatingWidget = { + organization, + widget: newObject, + zoneId: selectedZone.zoneId, + }; + + if (visualizationSocket) { + visualizationSocket.emit("v2:viz-float:add", addFloatingWidget); + } + + useDroppedObjectsStore + .getState() + .addObject(selectedZone.zoneName, newObject); + + const droppedObjectsStore = useDroppedObjectsStore.getState(); + const currentZone = droppedObjectsStore.zones[selectedZone.zoneName]; + + if (currentZone && currentZone.zoneId === selectedZone.zoneId) { + console.log( + `Objects for Zone ${selectedZone.zoneId}:`, + currentZone.objects + ); + setFloatingWidget(currentZone.objects); + } else { + console.warn("Zone not found or zoneId mismatch"); + } + } catch (error) { + console.error("Error in handleDrop:", error); + } +}; diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index bdefb8b..17a6315 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -13,6 +13,7 @@ import { useWallItems, useZones, useLoadingProgress, + useWidgetSubOption, } from "../store/store"; import { useNavigate } from "react-router-dom"; import { usePlayButtonStore } from "../store/usePlayButtonStore"; @@ -22,12 +23,19 @@ import SimulationPlayer from "../components/ui/simulation/simulationPlayer"; import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys"; import { useSelectedUserStore } from "../store/useCollabStore"; import FollowPerson from "../components/templates/FollowPerson"; -import ProductionCapacity from "../components/ui/analysis/ProductionCapacity"; -import ThroughputSummary from "../components/ui/analysis/ThroughputSummary"; -import ROISummary from "../components/ui/analysis/ROISummary"; +import Scene from "../modules/scene/scene"; +import { createHandleDrop } from "../modules/visualization/functions/handleUiDrop"; +import { useSelectedZoneStore } from "../store/visualization/useZoneStore"; +import { useFloatingWidget } from "../store/visualization/useDroppedObjectsStore"; +import { useLogger } from "../components/ui/log/LoggerContext"; +import RenderOverlay from "../components/templates/Overlay"; +import LogList from "../components/ui/log/LogList"; +import Footer from "../components/footer/Footer"; const Project: React.FC = () => { let navigate = useNavigate(); + const echo = useLogger(); + const { activeModule, setActiveModule } = useModuleStore(); const { loadingProgress } = useLoadingProgress(); const { setUserName } = useUserName(); @@ -50,42 +58,79 @@ const Project: React.FC = () => { setOrganization(Organization); setUserName(name); } + echo.info("Log in success full"); } else { navigate("/"); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const { isPlaying } = usePlayButtonStore(); + // global store const { toggleThreeD } = useThreeDStore(); + + // simulation store + const { isPlaying } = usePlayButtonStore(); + + // collaboration store const { selectedUser } = useSelectedUserStore(); + const { isLogListVisible } = useLogger(); + + // real-time visualization store + const { widgetSubOption } = useWidgetSubOption(); + const { visualizationSocket } = useSocketStore(); + const { selectedZone } = useSelectedZoneStore(); + const { setFloatingWidget } = useFloatingWidget(); return (
- {/*
-
- - -
- -
*/} - - {loadingProgress > 0 && } - {!isPlaying && ( + {!selectedUser && ( <> - {toggleThreeD && } - - + + {loadingProgress > 0 && } + {!isPlaying && ( + <> + {toggleThreeD && } + + + + )} + + {activeModule === "market" && } + {activeModule !== "market" && } + {isPlaying && activeModule === "simulation" && } )} - {/* - - */} - {activeModule === "market" && } - - {activeModule !== "market" && } - {isPlaying && activeModule === "simulation" && } - {/* {} */} +
+ createHandleDrop({ + widgetSubOption, + visualizationSocket, + selectedZone, + setFloatingWidget, + event, + }) + } + onDragOver={(event) => event.preventDefault()} + > + +
{selectedUser && } + {isLogListVisible && ( + + + + )} +
); }; diff --git a/app/src/services/simulation/renameProductApi.ts b/app/src/services/simulation/renameProductApi.ts new file mode 100644 index 0000000..afa493c --- /dev/null +++ b/app/src/services/simulation/renameProductApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const renameProductApi = async (body: { productName: string, productId: string, organization: string }) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v2/productRename`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error("Failed to rename product"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; diff --git a/app/src/store/simulation/useArmBotStore.ts b/app/src/store/simulation/useArmBotStore.ts index 642762f..3c54596 100644 --- a/app/src/store/simulation/useArmBotStore.ts +++ b/app/src/store/simulation/useArmBotStore.ts @@ -10,6 +10,7 @@ interface ArmBotStore { modelUuid: string, updates: Partial> ) => void; + clearArmBots: () => void; addCurrentAction: (modelUuid: string, actionUuid: string) => void; removeCurrentAction: (modelUuid: string) => void; @@ -39,14 +40,17 @@ export const useArmBotStore = create()( addArmBot: (productId, event) => { set((state) => { - state.armBots.push({ - ...event, - productId, - isActive: false, - idleTime: 0, - activeTime: 0, - state: 'idle', - }); + const exists = state.armBots.some(a => a.modelUuid === event.modelUuid); + if (!exists) { + state.armBots.push({ + ...event, + productId, + isActive: false, + idleTime: 0, + activeTime: 0, + state: 'idle', + }); + } }); }, @@ -65,6 +69,12 @@ export const useArmBotStore = create()( }); }, + clearArmBots: () => { + set((state) => { + state.armBots = []; + }); + }, + addCurrentAction: (modelUuid, actionUuid) => { set((state) => { const armBot = state.armBots.find(a => a.modelUuid === modelUuid); diff --git a/app/src/store/simulation/useConveyorStore.ts b/app/src/store/simulation/useConveyorStore.ts index 15dbf34..862ce79 100644 --- a/app/src/store/simulation/useConveyorStore.ts +++ b/app/src/store/simulation/useConveyorStore.ts @@ -10,6 +10,7 @@ interface ConveyorStore { modelUuid: string, updates: Partial> ) => void; + clearConveyors: () => void; setConveyorActive: (modelUuid: string, isActive: boolean) => void; setConveyorState: (modelUuid: string, newState: ConveyorStatus['state']) => void; @@ -30,14 +31,17 @@ export const useConveyorStore = create()( addConveyor: (productId, event) => { set((state) => { - state.conveyors.push({ - ...event, - productId, - isActive: false, - idleTime: 0, - activeTime: 0, - state: 'idle', - }); + const exists = state.conveyors.some(c => c.modelUuid === event.modelUuid); + if (!exists) { + state.conveyors.push({ + ...event, + productId, + isActive: false, + idleTime: 0, + activeTime: 0, + state: 'idle', + }); + } }); }, @@ -56,6 +60,12 @@ export const useConveyorStore = create()( }); }, + clearConveyors: () => { + set((state) => { + state.conveyors = []; + }); + }, + setConveyorActive: (modelUuid, isActive) => { set((state) => { const conveyor = state.conveyors.find(c => c.modelUuid === modelUuid); diff --git a/app/src/store/simulation/useEventsStore.ts b/app/src/store/simulation/useEventsStore.ts index 2d92fc2..5580eb1 100644 --- a/app/src/store/simulation/useEventsStore.ts +++ b/app/src/store/simulation/useEventsStore.ts @@ -7,7 +7,7 @@ type EventsStore = { // Event-level actions addEvent: (event: EventsSchema) => void; removeEvent: (modelUuid: string) => void; - updateEvent: (modelUuid: string, updates: Partial) => void; + updateEvent: (modelUuid: string, updates: Partial) => EventsSchema | undefined; // Point-level actions addPoint: (modelUuid: string, point: ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema) => void; @@ -49,7 +49,9 @@ export const useEventsStore = create()( // Event-level actions addEvent: (event) => { set((state) => { - state.events.push(event); + if (!state.events.some(e => 'modelUuid' in e && e.modelUuid === event.modelUuid)) { + state.events.push(event); + } }); }, @@ -60,12 +62,15 @@ export const useEventsStore = create()( }, updateEvent: (modelUuid, updates) => { + let updatedEvent: EventsSchema | undefined; set((state) => { const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); if (event) { Object.assign(event, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); } }); + return updatedEvent; }, // Point-level actions @@ -73,9 +78,14 @@ export const useEventsStore = create()( set((state) => { const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); if (event && 'points' in event) { - (event as ConveyorEventSchema).points.push(point as ConveyorPointSchema); + const existingPoint = (event as ConveyorEventSchema).points.find(p => p.uuid === point.uuid); + if (!existingPoint) { + (event as ConveyorEventSchema).points.push(point as ConveyorPointSchema); + } } else if (event && 'point' in event) { - (event as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point = point as any; + if (!(event as any).point || (event as any).point.uuid !== point.uuid) { + (event as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point = point as any; + } } }); }, @@ -110,14 +120,15 @@ export const useEventsStore = create()( const event = state.events.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); if (event && 'points' in event) { const point = (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); - if (point) { + if (point && (!point.action || point.action.actionUuid !== action.actionUuid)) { point.action = action as any; } } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { - if ('action' in (event as any).point) { - (event as any).point.action = action; - } else if ('actions' in (event as any).point) { - (event as any).point.actions.push(action); + const point = (event as any).point; + if ('action' in point && (!point.action || point.action.actionUuid !== action.actionUuid)) { + point.action = action; + } else if ('actions' in point && !point.actions.some((a: any) => a.actionUuid === action.actionUuid)) { + point.actions.push(action); } } }); @@ -180,18 +191,22 @@ export const useEventsStore = create()( if ('points' in event) { for (const point of (event as ConveyorEventSchema).points) { if (point.action && point.action.actionUuid === actionUuid) { - point.action.triggers.push(trigger); + if (!point.action.triggers.some(t => t.triggerUuid === trigger.triggerUuid)) { + point.action.triggers.push(trigger); + } return; } } } else if ('point' in event) { - const point = (event as any).point; + const point: MachinePointSchema | VehiclePointSchema = (event as any).point; if ('action' in point && point.action.actionUuid === actionUuid) { - point.action.triggers.push(trigger); + if (!point.action.triggers.some(t => t.triggerUuid === trigger.triggerUuid)) { + point.action.triggers.push(trigger); + } return; } else if ('actions' in point) { - const action = point.actions.find((a: any) => a.actionUuid === actionUuid); - if (action) { + const action = (point as RoboticArmPointSchema).actions.find((a) => a.actionUuid === actionUuid); + if (action && !action.triggers.some(t => t.triggerUuid === trigger.triggerUuid)) { action.triggers.push(trigger); return; } diff --git a/app/src/store/simulation/useMachineStore.ts b/app/src/store/simulation/useMachineStore.ts index cc927f7..9a564ac 100644 --- a/app/src/store/simulation/useMachineStore.ts +++ b/app/src/store/simulation/useMachineStore.ts @@ -4,23 +4,23 @@ import { immer } from 'zustand/middleware/immer'; interface MachineStore { machines: MachineStatus[]; - // Actions addMachine: (productId: string, machine: MachineEventSchema) => void; removeMachine: (modelUuid: string) => void; updateMachine: ( modelUuid: string, updates: Partial> ) => void; + clearMachines: () => void; + + addCurrentAction: (modelUuid: string, actionUuid: string) => void; + removeCurrentAction: (modelUuid: string) => void; - // Status updates setMachineActive: (modelUuid: string, isActive: boolean) => void; setMachineState: (modelUuid: string, newState: MachineStatus['state']) => void; - // Time tracking incrementActiveTime: (modelUuid: string, incrementBy: number) => void; incrementIdleTime: (modelUuid: string, incrementBy: number) => void; - // Helpers getMachineById: (modelUuid: string) => MachineStatus | undefined; getMachinesByProduct: (productId: string) => MachineStatus[]; getMachinesBystate: (state: string) => MachineStatus[]; @@ -32,17 +32,19 @@ export const useMachineStore = create()( immer((set, get) => ({ machines: [], - // Actions addMachine: (productId, machine) => { set((state) => { - state.machines.push({ - ...machine, - productId, - isActive: false, - idleTime: 0, - activeTime: 0, - state: 'idle', - }); + const exists = state.machines.some(m => m.modelUuid === machine.modelUuid); + if (!exists) { + state.machines.push({ + ...machine, + productId, + isActive: false, + idleTime: 0, + activeTime: 0, + state: 'idle', + }); + } }); }, @@ -61,7 +63,36 @@ export const useMachineStore = create()( }); }, - // Status updates + clearMachines: () => { + set((state) => { + state.machines = []; + }); + }, + + addCurrentAction: (modelUuid) => { + set((state) => { + const armBot = state.machines.find(a => a.modelUuid === modelUuid); + if (armBot) { + const action = armBot.point.action; + if (action) { + armBot.currentAction = { + actionUuid: action.actionUuid, + actionName: action.actionName, + }; + } + } + }); + }, + + removeCurrentAction: (modelUuid) => { + set((state) => { + const armBot = state.machines.find(a => a.modelUuid === modelUuid); + if (armBot) { + armBot.currentAction = undefined; + } + }); + }, + setMachineActive: (modelUuid, isActive) => { set((state) => { const machine = state.machines.find(m => m.modelUuid === modelUuid); @@ -80,7 +111,6 @@ export const useMachineStore = create()( }); }, - // Time tracking incrementActiveTime: (modelUuid, incrementBy) => { set((state) => { const machine = state.machines.find(m => m.modelUuid === modelUuid); @@ -99,7 +129,6 @@ export const useMachineStore = create()( }); }, - // Helpers getMachineById: (modelUuid) => { return get().machines.find(m => m.modelUuid === modelUuid); }, diff --git a/app/src/store/simulation/useProductStore.ts b/app/src/store/simulation/useProductStore.ts index 4f2b546..8b83cdd 100644 --- a/app/src/store/simulation/useProductStore.ts +++ b/app/src/store/simulation/useProductStore.ts @@ -33,27 +33,30 @@ type ProductsStore = { pointUuid: string, action: ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action'] ) => EventsSchema | undefined; - removeAction: (actionUuid: string) => EventsSchema | undefined; + removeAction: (productId: string, actionUuid: string) => EventsSchema | undefined; updateAction: ( + productId: string, actionUuid: string, updates: Partial ) => EventsSchema | undefined; // Trigger-level actions addTrigger: ( + productId: string, actionUuid: string, trigger: TriggerSchema - ) => void; - removeTrigger: (triggerUuid: string) => void; + ) => EventsSchema | undefined; + removeTrigger: (productId: string, triggerUuid: string) => EventsSchema | undefined; updateTrigger: ( + productId: string, triggerUuid: string, updates: Partial - ) => void; + ) => EventsSchema | undefined; // Renaming functions renameProduct: (productId: string, newName: string) => void; - renameAction: (actionUuid: string, newName: string) => EventsSchema | undefined; - renameTrigger: (triggerUuid: string, newName: string) => void; + renameAction: (productId: string, actionUuid: string, newName: string) => EventsSchema | undefined; + renameTrigger: (productId: string, triggerUuid: string, newName: string) => void; // Helper functions getProductById: (productId: string) => { productName: string; productId: string; eventDatas: EventsSchema[] } | undefined; @@ -71,12 +74,15 @@ export const useProductStore = create()( // Product-level actions addProduct: (productName, productId) => { set((state) => { - const newProduct = { - productName, - productId: productId, - eventDatas: [] - }; - state.products.push(newProduct); + const existingProduct = state.products.find(p => p.productId === productId); + if (!existingProduct) { + const newProduct = { + productName, + productId: productId, + eventDatas: [] + }; + state.products.push(newProduct); + } }); }, @@ -106,7 +112,10 @@ export const useProductStore = create()( set((state) => { const product = state.products.find(p => p.productId === productId); if (product) { - product.eventDatas.push(event); + const existingEvent = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === event.modelUuid); + if (!existingEvent) { + product.eventDatas.push(event); + } } }); }, @@ -120,7 +129,7 @@ export const useProductStore = create()( }); }, - deleteEvent: (modelUuid: string) => { + deleteEvent: (modelUuid) => { set((state) => { for (const product of state.products) { product.eventDatas = product.eventDatas.filter(e => 'modelUuid' in e && e.modelUuid !== modelUuid); @@ -150,9 +159,15 @@ export const useProductStore = create()( if (product) { const event = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); if (event && 'points' in event) { - (event as ConveyorEventSchema).points.push(point as ConveyorPointSchema); + const existingPoint = (event as ConveyorEventSchema).points.find(p => p.uuid === point.uuid); + if (!existingPoint) { + (event as ConveyorEventSchema).points.push(point as ConveyorPointSchema); + } } else if (event && 'point' in event) { - (event as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point = point as any; + const existingPoint = (event as any).point?.uuid === point.uuid; + if (!existingPoint) { + (event as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point = point as any; + } } } }); @@ -198,17 +213,22 @@ export const useProductStore = create()( const event = product.eventDatas.find(e => 'modelUuid' in e && e.modelUuid === modelUuid); if (event && 'points' in event) { const point = (event as ConveyorEventSchema).points.find(p => p.uuid === pointUuid); - if (point) { + if (point && (!point.action || point.action.actionUuid !== action.actionUuid)) { point.action = action as any; updatedEvent = JSON.parse(JSON.stringify(event)); } } else if (event && 'point' in event && (event as any).point.uuid === pointUuid) { if ('action' in (event as any).point) { - (event as any).point.action = action; - updatedEvent = JSON.parse(JSON.stringify(event)); + if (!(event as any).point.action || (event as any).point.action.actionUuid !== action.actionUuid) { + (event as any).point.action = action; + updatedEvent = JSON.parse(JSON.stringify(event)); + } } else if ('actions' in (event as any).point) { - (event as any).point.actions.push(action); - updatedEvent = JSON.parse(JSON.stringify(event)); + const existingAction = (event as any).point.actions.find((a: any) => a.actionUuid === action.actionUuid); + if (!existingAction) { + (event as any).point.actions.push(action); + updatedEvent = JSON.parse(JSON.stringify(event)); + } } } } @@ -216,10 +236,11 @@ export const useProductStore = create()( return updatedEvent; }, - removeAction: (actionUuid: string) => { + removeAction: (productId, actionUuid) => { let updatedEvent: EventsSchema | undefined; set((state) => { - for (const product of state.products) { + const product = state.products.find(p => p.productId === productId); + if (product) { for (const event of product.eventDatas) { if ('points' in event) { // Handle ConveyorEventSchema @@ -248,10 +269,11 @@ export const useProductStore = create()( return updatedEvent; }, - updateAction: (actionUuid, updates) => { + updateAction: (productId, actionUuid, updates) => { let updatedEvent: EventsSchema | undefined; set((state) => { - for (const product of state.products) { + const product = state.products.find(p => p.productId === productId); + if (product) { for (const event of product.eventDatas) { if ('points' in event) { for (const point of (event as ConveyorEventSchema).points) { @@ -283,26 +305,40 @@ export const useProductStore = create()( }, // Trigger-level actions - addTrigger: (actionUuid, trigger) => { + addTrigger: (productId, actionUuid, trigger) => { + let updatedEvent: EventsSchema | undefined; set((state) => { - for (const product of state.products) { + const product = state.products.find(p => p.productId === productId); + if (product) { for (const event of product.eventDatas) { if ('points' in event) { for (const point of (event as ConveyorEventSchema).points) { if (point.action && point.action.actionUuid === actionUuid) { - point.action.triggers.push(trigger); + const existingTrigger = point.action.triggers.find(t => t.triggerUuid === trigger.triggerUuid); + if (!existingTrigger) { + point.action.triggers.push(trigger); + updatedEvent = JSON.parse(JSON.stringify(event)); + } return; } } } else if ('point' in event) { const point = (event as any).point; if ('action' in point && point.action.actionUuid === actionUuid) { - point.action.triggers.push(trigger); + const existingTrigger = point.action.triggers.find((t: any) => t.triggerUuid === trigger.triggerUuid); + if (!existingTrigger) { + point.action.triggers.push(trigger); + updatedEvent = JSON.parse(JSON.stringify(event)); + } return; } else if ('actions' in point) { const action = point.actions.find((a: any) => a.actionUuid === actionUuid); if (action) { - action.triggers.push(trigger); + const existingTrigger = action.triggers.find((t: any) => t.triggerUuid === trigger.triggerUuid); + if (!existingTrigger) { + action.triggers.push(trigger); + updatedEvent = JSON.parse(JSON.stringify(event)); + } return; } } @@ -310,26 +346,41 @@ export const useProductStore = create()( } } }); + return updatedEvent; }, - removeTrigger: (triggerUuid) => { + removeTrigger: (productId, triggerUuid) => { + let updatedEvent: EventsSchema | undefined; set((state) => { - for (const product of state.products) { + const product = state.products.find(p => p.productId === productId); + if (product) { for (const event of product.eventDatas) { if ('points' in event) { for (const point of (event as ConveyorEventSchema).points) { if (point.action && 'triggers' in point.action) { - point.action.triggers = point.action.triggers.filter(t => t.triggerUuid !== triggerUuid); + const Trigger = point.action.triggers.find(t => t.triggerUuid === triggerUuid); + if (Trigger) { + point.action.triggers = point.action.triggers.filter(t => t.triggerUuid !== triggerUuid); + updatedEvent = JSON.parse(JSON.stringify(event)); + } } } } else if ('point' in event) { const point = (event as any).point; if ('action' in point && 'triggers' in point.action) { - point.action.triggers = point.action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + const Trigger = point.action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (Trigger) { + point.action.triggers = point.action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + updatedEvent = JSON.parse(JSON.stringify(event)); + } } else if ('actions' in point) { for (const action of point.actions) { if ('triggers' in action) { - action.triggers = action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + const Trigger = action.triggers.find((t: any) => t.triggerUuid === triggerUuid); + if (Trigger) { + action.triggers = action.triggers.filter((t: any) => t.triggerUuid !== triggerUuid); + updatedEvent = JSON.parse(JSON.stringify(event)); + } } } } @@ -337,11 +388,14 @@ export const useProductStore = create()( } } }); + return updatedEvent; }, - updateTrigger: (triggerUuid, updates) => { + updateTrigger: (productId, triggerUuid, updates) => { + let updatedEvent: EventsSchema | undefined; set((state) => { - for (const product of state.products) { + const product = state.products.find(p => p.productId === productId); + if (product) { for (const event of product.eventDatas) { if ('points' in event) { for (const point of (event as ConveyorEventSchema).points) { @@ -349,6 +403,7 @@ export const useProductStore = create()( const trigger = point.action.triggers.find(t => t.triggerUuid === triggerUuid); if (trigger) { Object.assign(trigger, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); return; } } @@ -359,6 +414,7 @@ export const useProductStore = create()( const trigger = point.action.triggers.find((t: any) => t.triggerUuid === triggerUuid); if (trigger) { Object.assign(trigger, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); return; } } else if ('actions' in point) { @@ -367,6 +423,7 @@ export const useProductStore = create()( const trigger = action.triggers.find((t: any) => t.triggerUuid === triggerUuid); if (trigger) { Object.assign(trigger, updates); + updatedEvent = JSON.parse(JSON.stringify(event)); return; } } @@ -376,6 +433,7 @@ export const useProductStore = create()( } } }); + return updatedEvent; }, // Renaming functions @@ -388,10 +446,11 @@ export const useProductStore = create()( }); }, - renameAction: (actionUuid, newName) => { + renameAction: (productId, actionUuid, newName) => { let updatedEvent: EventsSchema | undefined; set((state) => { - for (const product of state.products) { + const product = state.products.find(p => p.productId === productId); + if (product) { for (const event of product.eventDatas) { if ('points' in event) { for (const point of (event as ConveyorEventSchema).points) { @@ -422,9 +481,10 @@ export const useProductStore = create()( return updatedEvent; }, - renameTrigger: (triggerUuid, newName) => { + renameTrigger: (productId, triggerUuid, newName) => { set((state) => { - for (const product of state.products) { + const product = state.products.find(p => p.productId === productId); + if (product) { for (const event of product.eventDatas) { if ('points' in event) { for (const point of (event as ConveyorEventSchema).points) { diff --git a/app/src/store/simulation/useSimulationStore.ts b/app/src/store/simulation/useSimulationStore.ts index 5085688..6e4321f 100644 --- a/app/src/store/simulation/useSimulationStore.ts +++ b/app/src/store/simulation/useSimulationStore.ts @@ -114,4 +114,36 @@ export const useSelectedAction = create()( }); }, })) +); + +interface IsDraggingState { + isDragging: "start" | "end" | null; + setIsDragging: (state: "start" | "end" | null) => void; +} + +export const useIsDragging = create()( + immer((set) => ({ + isDragging: null, + setIsDragging: (state) => { + set((s) => { + s.isDragging = state; + }); + }, + })) +); + +interface IsRotatingState { + isRotating: "start" | "end" | null; + setIsRotating: (state: "start" | "end" | null) => void; +} + +export const useIsRotating = create()( + immer((set) => ({ + isRotating: null, + setIsRotating: (state) => { + set((s) => { + s.isRotating = state; + }); + }, + })) ); \ No newline at end of file diff --git a/app/src/store/simulation/useStorageUnitStore.ts b/app/src/store/simulation/useStorageUnitStore.ts index d729708..aec2f12 100644 --- a/app/src/store/simulation/useStorageUnitStore.ts +++ b/app/src/store/simulation/useStorageUnitStore.ts @@ -4,26 +4,22 @@ import { immer } from 'zustand/middleware/immer'; interface StorageUnitStore { storageUnits: StorageUnitStatus[]; - // Actions addStorageUnit: (productId: string, storageUnit: StorageEventSchema) => void; removeStorageUnit: (modelUuid: string) => void; updateStorageUnit: ( modelUuid: string, updates: Partial> ) => void; + clearStorageUnits: () => void; - // Status updates setStorageUnitActive: (modelUuid: string, isActive: boolean) => void; setStorageUnitState: (modelUuid: string, newState: StorageUnitStatus['state']) => void; - // Load updates updateStorageUnitLoad: (modelUuid: string, incrementBy: number) => void; - // Time tracking incrementActiveTime: (modelUuid: string, incrementBy: number) => void; incrementIdleTime: (modelUuid: string, incrementBy: number) => void; - // Helpers getStorageUnitById: (modelUuid: string) => StorageUnitStatus | undefined; getStorageUnitsByProduct: (productId: string) => StorageUnitStatus[]; getStorageUnitsBystate: (state: string) => StorageUnitStatus[]; @@ -37,18 +33,20 @@ export const useStorageUnitStore = create()( immer((set, get) => ({ storageUnits: [], - // Actions addStorageUnit: (productId, storageUnit) => { set((state) => { - state.storageUnits.push({ - ...storageUnit, - productId, - isActive: false, - idleTime: 0, - activeTime: 0, - currentLoad: 0, - state: 'idle', - }); + const exists = state.storageUnits.some(s => s.modelUuid === storageUnit.modelUuid); + if (!exists) { + state.storageUnits.push({ + ...storageUnit, + productId, + isActive: false, + idleTime: 0, + activeTime: 0, + currentLoad: 0, + state: 'idle', + }); + } }); }, @@ -67,7 +65,12 @@ export const useStorageUnitStore = create()( }); }, - // Status updates + clearStorageUnits: () => { + set(() => ({ + storageUnits: [], + })); + }, + setStorageUnitActive: (modelUuid, isActive) => { set((state) => { const unit = state.storageUnits.find(s => s.modelUuid === modelUuid); @@ -86,7 +89,6 @@ export const useStorageUnitStore = create()( }); }, - // Load updates updateStorageUnitLoad: (modelUuid, incrementBy) => { set((state) => { const unit = state.storageUnits.find(s => s.modelUuid === modelUuid); @@ -96,7 +98,6 @@ export const useStorageUnitStore = create()( }); }, - // Time tracking incrementActiveTime: (modelUuid, incrementBy) => { set((state) => { const unit = state.storageUnits.find(s => s.modelUuid === modelUuid); @@ -115,7 +116,6 @@ export const useStorageUnitStore = create()( }); }, - // Helpers getStorageUnitById: (modelUuid) => { return get().storageUnits.find(s => s.modelUuid === modelUuid); }, diff --git a/app/src/store/simulation/useVehicleStore.ts b/app/src/store/simulation/useVehicleStore.ts index a5ea3be..ecef809 100644 --- a/app/src/store/simulation/useVehicleStore.ts +++ b/app/src/store/simulation/useVehicleStore.ts @@ -20,6 +20,7 @@ interface VehiclesStore { modelUuid: string, updates: Partial> ) => void; + clearvehicles: () => void; setVehicleActive: (modelUuid: string, isActive: boolean) => void; updateSteeringAngle: (modelUuid: string, steeringAngle: number) => void; @@ -41,15 +42,18 @@ export const useVehicleStore = create()( addVehicle: (productId, event) => { set((state) => { - state.vehicles.push({ - ...event, - productId, - isActive: false, - idleTime: 0, - activeTime: 0, - currentLoad: 0, - distanceTraveled: 0, - }); + const exists = state.vehicles.some(v => v.modelUuid === event.modelUuid); + if (!exists) { + state.vehicles.push({ + ...event, + productId, + isActive: false, + idleTime: 0, + activeTime: 0, + currentLoad: 0, + distanceTraveled: 0, + }); + } }); }, @@ -68,6 +72,12 @@ export const useVehicleStore = create()( }); }, + clearvehicles: () => { + set((state) => { + state.vehicles = []; + }); + }, + setVehicleActive: (modelUuid, isActive) => { set((state) => { const vehicle = state.vehicles.find(v => v.modelUuid === modelUuid); diff --git a/app/src/store/visualization/useDroppedObjectsStore.ts b/app/src/store/visualization/useDroppedObjectsStore.ts index 5c4527b..bbe4cde 100644 --- a/app/src/store/visualization/useDroppedObjectsStore.ts +++ b/app/src/store/visualization/useDroppedObjectsStore.ts @@ -1,5 +1,4 @@ import { create } from "zustand"; -import { addingFloatingWidgets } from "../../services/visulization/zone/addFloatingWidgets"; import { useSocketStore } from "../store"; import useChartStore from "./useChartStore"; diff --git a/app/src/styles/abstracts/variables.scss b/app/src/styles/abstracts/variables.scss index f4a6495..29976dc 100644 --- a/app/src/styles/abstracts/variables.scss +++ b/app/src/styles/abstracts/variables.scss @@ -22,7 +22,11 @@ $text-button-color-dark: #f3f3fd; // background colors // ---------- light mode ---------- $background-color: linear-gradient(-45deg, #fcfdfd71 0%, #fcfdfd79 100%); -$background-color-solid-gradient: linear-gradient(-45deg, #fcfdfd 0%, #fcfdfd 100%); +$background-color-solid-gradient: linear-gradient( + -45deg, + #fcfdfd 0%, + #fcfdfd 100% +); $background-color-solid: #fcfdfd; $background-color-secondary: #fcfdfd4d; $background-color-accent: #6f42c1; @@ -45,7 +49,11 @@ $background-radial-gray-gradient: radial-gradient( // ---------- dark mode ---------- $background-color-dark: linear-gradient(-45deg, #333333b3 0%, #2d2437b3 100%); -$background-color-solid-gradient-dark: linear-gradient(-45deg, #333333 0%, #2d2437 100%); +$background-color-solid-gradient-dark: linear-gradient( + -45deg, + #333333 0%, + #2d2437 100% +); $background-color-solid-dark: #19191d; $background-color-secondary-dark: #19191d99; $background-color-accent-dark: #6f42c1; @@ -104,6 +112,21 @@ $color3: #b186ff; $color4: #8752e8; $color5: #c7a8ff; +// log indication colors +// ------------ text ------------- +$log-default-text-color: #6f42c1; +$log-info-text-color: #488ef6; +$log-warn-text-color: #f3a50c; +$log-error-text-color: #f65648; +$log-success-text-color: #43c06d; + +// ------------ background ------------- +$log-default-backgroung-color: #6e42c133; +$log-info-background-color: #488ef633; +$log-warn-background-color: #f3a50c33; +$log-error-background-color: #f6564833; +$log-success-background-color: #43c06d33; + // old variables $accent-color: #6f42c1; $accent-color-dark: #c4abf1; diff --git a/app/src/styles/base/base.scss b/app/src/styles/base/base.scss index bf53164..4555ed9 100644 --- a/app/src/styles/base/base.scss +++ b/app/src/styles/base/base.scss @@ -37,11 +37,9 @@ // old colors --accent-color: #{$accent-color}; - --highlight-accent-color: #{$highlight-accent-color}; --accent-gradient-color: #{$acent-gradient}; --faint-gradient-color: #{$faint-gradient}; --background-color-gray: #{$background-color-gray}; - --border-color: #{$border-color}; --shadow-main-light: #{$shadow-color}; --box-shadow-light: 0px 2px 4px var(--shadow-main-light); --box-shadow-medium: 0px 4px 8px var(--shadow-main-light); @@ -75,7 +73,7 @@ --background-radial-gray-gradient: #{$background-radial-gray-gradient-dark}; // border colors - --border-color: #{$border-color}; + --border-color: #{$border-color-dark}; --input-border-color: #{$input-border-color-dark}; --border-color-accent: #{$border-color-accent-dark}; @@ -89,11 +87,9 @@ // old colors --accent-color: #{$accent-color-dark}; - --highlight-accent-color: #{$highlight-accent-color-dark}; --accent-gradient-color: #{$acent-gradient-dark}; --faint-gradient-color: #{$faint-gradient-dark}; --background-color-gray: #{$background-color-gray-dark}; - --border-color: #{$border-color-dark}; --shadow-main-dark: #{$shadow-color}; --box-shadow-light: 0px 2px 4px var(--shadow-main-dark); --box-shadow-medium: 0px 4px 8px var(--shadow-main-dark); diff --git a/app/src/styles/base/global.scss b/app/src/styles/base/global.scss index d90e6fb..f13344e 100644 --- a/app/src/styles/base/global.scss +++ b/app/src/styles/base/global.scss @@ -1,11 +1,30 @@ @use "../abstracts/variables" as *; @use "../abstracts/mixins" as *; -section, .section{ - padding: 4px; - outline: 1px solid var(--border-color); - outline-offset: -1px; - border-radius: #{$border-radius-large}; - background: var(--background-color); - margin: 4px 0; +section, +.section { + padding: 4px; + outline: 1px solid var(--border-color); + outline-offset: -1px; + border-radius: #{$border-radius-large}; + background: var(--background-color); + margin: 4px 0; +} + +.scene-container { + width: calc(100% - (320px + 270px + 90px)); + height: calc(100% - (250px)); + position: absolute; + top: 50%; + left: calc(270px + 45px); + overflow: hidden; + z-index: 1; + transform: translate(0, -50%); + transition: all 0.2s; + box-shadow: $box-shadow-medium; + background: var(--background-color-solid); + canvas { + outline: none; + border: none; + } } diff --git a/app/src/styles/base/reset.scss b/app/src/styles/base/reset.scss index ab77f9a..82d286e 100644 --- a/app/src/styles/base/reset.scss +++ b/app/src/styles/base/reset.scss @@ -12,10 +12,3 @@ input[type="password"]::-webkit-clear-button, /* For Chrome/Safari clear button input[type="password"]::-webkit-inner-spin-button { /* Just in case */ display: none; } - -button{ - border: none; - outline: none; - background: none; - cursor: pointer; -} \ No newline at end of file diff --git a/app/src/styles/components/analysis/ROISummary.scss b/app/src/styles/components/analysis/ROISummary.scss deleted file mode 100644 index 96b4a5d..0000000 --- a/app/src/styles/components/analysis/ROISummary.scss +++ /dev/null @@ -1,311 +0,0 @@ -.roiSummary-container { - .roiSummary-wrapper { - background-color: var(--background-color); - - .product-info { - display: flex; - } - - .playBack { - display: flex; - background-color: var(--background-color); - border-radius: 12px; - padding: 6px; - - .info { - span { - font-size: var(--font-size-xlarge); - - &:first-child { - color: #31C756; - } - - &:last-child { - color: var(--text-color); - } - } - } - } - - .roi-details { - display: flex; - align-items: center; - gap: 12px; - - .progress-wrapper { - width: 250px; - display: flex; - flex-direction: column; - gap: 6px; - - .content { - display: flex; - flex-direction: column; - gap: 3px; - align-items: center; - - .key { - font-size: var(--font-size-xlarge); - color: var(--accent-color); - } - } - } - - .roi-progress { - width: 100%; - } - - .metrics { - display: flex; - flex-direction: column; - gap: 6px; - - .metric-item { - width: 100%; - border-radius: 6px; - border: 1px solid #00FF56; - background: #436D51; - display: flex; - flex-direction: column; - padding: 4px 6px; - - &:last-child { - align-items: center; - } - - .metric-label { - font-size: 10px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .metric-value { - text-align: center; - line-height: 20px; - } - } - - .metric-wrapper { - display: flex; - gap: 6px; - - .metric-item { - background-color: var(--background-color); - border: 1px solid var(--Grays-Gray-6, #F2F2F7); - } - } - } - } - - .cost-breakdown { - background-color: var(--background-color); - border: 1px solid var(--text-disabled); - border-radius: 8px; - padding: 16px; - - .breakdown-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 16px; - - .section-wrapper { - display: flex; - gap: 4px; - align-items: center; - } - - .section-number { - font-size: 20px; - color: #00aaff; - } - - .section-title { - font-size: var(--font-size-regular); - color: var(--text-color); - } - - .expand-icon { - font-size: 16px; - color: var(--text-color); - cursor: pointer; - transform: rotate(90deg); - transition: transform 0.2s linear; - } - - .expand-icon.open { - transform: rotate(0deg); - - } - } - - .breakdown-table { - width: 100%; - border-collapse: collapse; - border-radius: 8px; - - th, - td { - padding: 8px; - text-align: left; - border-top: 1px solid var(--text-disabled); - border-bottom: 1px solid var(--text-disabled); - } - - th:first-child, - td:first-child { - border-left: 1px solid var(--text-disabled); - } - - th:last-child, - td:last-child { - border-right: 1px solid var(--text-disabled); - } - - th { - background-color: var(--background-color); - color: #333; - } - - .total-row, - .net-profit-row { - font-weight: bold; - color: #333; - } - } - } - - .tips-section { - background-color: var(--background-color); - border-radius: 8px; - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px; - - .tip-header { - display: flex; - align-items: center; - - .tip-title { - color: var(--text-color); - font-weight: 600; - } - } - - .tip-description { - span { - font-size: var(--font-size-xlarge); - color: #34C759; - - &:first-child { - color: #34C759; - } - - &:nth-child(2) { - color: #488EF6; - } - } - } - } - - .get-tips-button { - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 14px; - margin-top: 8px; - display: inline-block; - display: flex; - justify-content: flex-end; - background: none; - - .btn { - background-color: var(--accent-color); - color: var(--background-color); - padding: 4px 6px; - border-radius: 5px; - display: inline-block; - font-size: 14px; - text-align: center; - } - } - } - - .semi-circle-wrapper { - width: 100%; - height: 125px; - overflow-y: hidden; - position: relative; - .semi-circle { - width: 100%; - height: 250px; - border-radius: 50%; - position: relative; - transition: background 0.5s ease; - } - .progress-cover { - position: absolute; - width: 75%; - height: 75%; - top: 12.5%; - left: 12.5%; - background: #000000cc; - border-radius: 50%; - } - } - - - - .label-wrapper { - .label { - font-size: var(--font-size-xxxlarge); - } - - position: absolute; - bottom: 0%; - left: 50%; - transform: translate(-50%, 0%); - font-weight: bold; - font-size: 1.2rem; - color: #333; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - } -} - -// Breakdown Table Open/Close Logic - -.breakdown-table-wrapper { - &.closed { - max-height: 0; - padding: 0; - } - - &.open { - max-height: 500px; - } - - - - - - .breakdown-table { - width: 100%; - border-collapse: collapse; - - th, - td { - padding: 10px; - border: 1px solid #ddd; - text-align: left; - } - - - - } -} \ No newline at end of file diff --git a/app/src/styles/components/analysis/analysis.scss b/app/src/styles/components/analysis/analysis.scss deleted file mode 100644 index 030a79f..0000000 --- a/app/src/styles/components/analysis/analysis.scss +++ /dev/null @@ -1,279 +0,0 @@ -.analysis { - position: fixed; - top: 0; - left: 0; - display: flex; - justify-content: space-between; - align-items: start; - width: 100%; - height: 100vh; - // pointer-events: none; - z-index: 10000; - - .analysis-wrapper { - display: flex; - flex-direction: column; - gap: 12px; - } -} - -.analysis-card { - min-width: 333px; - background: var(--background-color); - border-radius: 20px; - - padding: 8px; - - .analysis-card-wrapper { - width: 100%; - background: var(--background-color); - border-radius: 14px; - padding: 16px; - - display: flex; - flex-direction: column; - gap: 14px; - - .card-header { - width: 100%; - display: flex; - justify-content: space-between; - align-items: center; - - .main-header { - line-height: 20px; - font-size: var(--font-size-regular); - } - } - - .process-container { - display: flex; - flex-direction: column; - - .throughput-value { - font-size: 1rem; - - .value { - font-weight: bold; - font-size: 1.5rem; - } - } - - .progress-bar-wrapper { - display: flex; - gap: 8px; - margin-top: 6px; - } - - .progress-bar { - position: relative; - // width: 36px; - width: 100%; - height: 4px; - border-radius: 13px; - overflow: hidden; - background: #FBEBD7; - - .bar-fill { - position: absolute; - height: 100%; - top: 0; - left: 0; - background: #FC9D2F; - border-radius: 13px; - } - - .bar-fill.full { - width: 100%; - } - - .bar-fill.partial { - width: 0; // inline style will override this - } - } - } - - .metrics-section { - padding-top: 16px; - border-top: 1px solid var(--background-color-gray); - - .metric { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 14px; - margin-bottom: 8px; - - .label { - color: var(--text-color); - } - - .value { - font-weight: bold; - } - } - } - } -} - - -.throughoutSummary { - .throughoutSummary-wrapper { - .process-container { - display: flex; - flex-direction: row; - align-items: center; - justify-content: flex-start; - gap: 16px; - width: 100%; - - .throughput-value { - font-size: var(--font-size-small); - flex: 1; - display: flex; - flex-direction: column; - - .value { - color: var(--accent-color); - } - - /* Let the text take available space */ - } - - .lineChart { - max-width: 200px; - height: 100px; - position: relative; - - .assetUsage { - text-align: right; - position: absolute; - right: 0; - top: 0; - } - - canvas { - background: transparent; - } - } - } - - .footer { - display: flex; - gap: 16px; // Space between cards - margin-top: 24px; - - .footer-card { - width: 100%; - background: var(--background-color-gray); - border-radius: 6px; - padding: 8px; - display: flex; - flex-direction: column; - gap: 6px; - - &:first-child { - width: 85%; - } - - .header { - font-size: var(--font-size-regular); - } - - .value-container { - display: flex; - flex-direction: row; - align-items: center; - justify-content: end; - gap: 6px; - } - } - - .shiftUtilization { - .value-container { - flex-direction: column; - align-items: flex-start; - justify-content: flex-start; - - .value { - font-size: var(--font-size-xlarge); - } - - .progress-wrapper { - width: 100%; - display: flex; - gap: 6px; - - .progress { - border-radius: 6px; - height: 5px; - - &:nth-child(1) { - background: #F3C64D; - } - - &:nth-child(2) { - background: #67B3F4; - } - - &:nth-child(3) { - background: #7981F5; - } - } - } - - .progress-indicator { - display: flex; - justify-content: space-between; - width: 100%; - gap: 6px; - - .shift-wrapper { - display: flex; - align-items: center; - gap: 5px; - - /* Align items vertically */ - &:nth-child(1) { - .indicator { - - background: #F3C64D; - } - } - - &:nth-child(2) { - .indicator { - - background: #67B3F4; - } - } - - &:nth-child(3) { - .indicator { - - background: #7981F5; - } - } - - label { - font-size: var(--font-size-small); - position: relative; - } - - .indicator { - display: inline-block; - width: 5px; - height: 5px; - border-radius: 50%; - - } - } - } - } - } - - } - - - } -} \ No newline at end of file diff --git a/app/src/styles/components/button.scss b/app/src/styles/components/button.scss index e69de29..dad9120 100644 --- a/app/src/styles/components/button.scss +++ b/app/src/styles/components/button.scss @@ -0,0 +1,24 @@ +@use "../abstracts/variables" as *; +@use "../abstracts/mixins" as *; + +.labeled-button-container { + @include flex-space-between; + padding: 6px 12px; + + button { + padding: 2px 32px; + border: none; + border-radius: #{$border-radius-large}; + color: var(--text-button-color); + background: var(--background-color-button); + transition: all 0.2s; + cursor: pointer; + } +} + +button { + border: none; + outline: none; + background: none; + cursor: pointer; +} diff --git a/app/src/styles/components/footer/footer.scss b/app/src/styles/components/footer/footer.scss new file mode 100644 index 0000000..b2d85d0 --- /dev/null +++ b/app/src/styles/components/footer/footer.scss @@ -0,0 +1,71 @@ +@use "../../abstracts/variables" as *; +@use "../../abstracts/mixins" as *; + +.footer-wrapper { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + z-index: 1; + display: flex; + justify-content: space-between; + padding: 2px 12px; + + .selection-wrapper { + display: flex; + gap: 6px; + + .selector-wrapper { + display: flex; + gap: 6px; + align-items: center; + background: var(--background-color); + padding: 3px 6px; + border-radius: 12px; + color: var(--text-color); + + .selector { + color: var(--text-color); + } + } + } + + .logs-wrapper { + display: flex; + gap: 6px; + + .logs-detail, + .version { + border-radius: 12px; + background: var(--background-color); + padding: 3px 6px; + color: var(--text-color); + display: flex; + align-items: center; + gap: 6px; + } + + .logs-detail { + padding: 2px 12px; + cursor: pointer; + .log-icon { + @include flex-center; + } + .log-message { + max-width: 40vw; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .version { + font-size: var(--font-size-tiny); + display: flex; + gap: 6px; + .icon{ + @include flex-center; + } + } + } +} diff --git a/app/src/styles/components/input.scss b/app/src/styles/components/input.scss index e3b9585..d4c6544 100644 --- a/app/src/styles/components/input.scss +++ b/app/src/styles/components/input.scss @@ -639,21 +639,6 @@ input[type="number"] { } } -.labeled-button-container { - @include flex-space-between; - padding: 6px 12px; - - button { - padding: 2px 32px; - border: none; - border-radius: #{$border-radius-large}; - color: var(--text-button-color); - background: var(--background-color-button); - transition: all 0.2s; - cursor: pointer; - } -} - .value-field-container { margin-bottom: 6px; padding: 6px 12px; diff --git a/app/src/styles/components/layouts.scss b/app/src/styles/components/layouts.scss deleted file mode 100644 index e69de29..0000000 diff --git a/app/src/styles/components/logs/logs.scss b/app/src/styles/components/logs/logs.scss new file mode 100644 index 0000000..f07708b --- /dev/null +++ b/app/src/styles/components/logs/logs.scss @@ -0,0 +1,105 @@ +@use "../../abstracts/variables" as *; +@use "../../abstracts/mixins" as *; + +.log-list-container { + width: 100vw; + height: 100vh; + background: var(--background-color-secondary); + + .log-list-wrapper { + height: 50%; + min-width: 50%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 5; + background: var(--background-color); + padding: 14px 12px; + border-radius: 15px; + display: flex; + flex-direction: column; + gap: 12px; + backdrop-filter: blur(50px); + outline: 1px solid var(--border-color); + + .log-header { + display: flex; + justify-content: space-between; + + .log-header-wrapper { + display: flex; + align-items: center; + gap: 6px; + } + + .close { + @include flex-center; + height: 28px; + width: 28px; + cursor: pointer; + svg { + scale: 1.6; + } + } + } + + .log-nav-wrapper { + display: flex; + gap: 6px; + + .log-nav { + padding: 8px 16px; + border-radius: 19px; + } + + .log-nav.active { + background-color: var(--background-color-accent); + color: var(--text-button-color); + } + } + + .log-entry-wrapper { + height: 100%; + display: flex; + flex-direction: column; + gap: 4px; + background: var(--background-color); + padding: 18px 10px; + border-radius: 16px; + outline: 1px solid var(--border-color); + outline-offset: -1px; + + .log-entry { + padding: 4px; + border-radius: 4px; + font-size: var(--font-size-small); + display: flex; + align-items: center; + gap: 6px; + + .log-icon { + @include flex-center; + } + .log-entry-message-container { + @include flex-space-between; + gap: 12px; + width: 100%; + .message-time { + font-size: var(--font-size-tiny); + font-weight: 300; + opacity: 0.8; + text-wrap: nowrap; + } + .log-entry-message{ + width: 100%; + } + } + + &:nth-child(odd) { + background: var(--background-color); + } + } + } + } +} diff --git a/app/src/styles/components/simulation/analysis.scss b/app/src/styles/components/simulation/analysis.scss new file mode 100644 index 0000000..9aed5c8 --- /dev/null +++ b/app/src/styles/components/simulation/analysis.scss @@ -0,0 +1,557 @@ +@use "../../abstracts/variables" as *; +@use "../../abstracts/mixins" as *; + +.analysis { + position: fixed; + top: 0; + left: 0; + display: flex; + justify-content: space-between; + align-items: start; + width: 100%; + height: 100vh; + pointer-events: none; + padding: 10px; + z-index: 2; + + .analysis-wrapper { + display: flex; + flex-direction: column; + gap: 12px; + } + .analysis-card { + min-width: 333px; + border-radius: 20px; + padding: 8px; + pointer-events: all; + + .analysis-card-wrapper { + width: 100%; + background: var(--background-color); + border-radius: 14px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 14px; + backdrop-filter: blur(10px); + outline: 1px solid var(--border-color); + outline-offset: -1px; + + .card-header { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + + .main-header { + line-height: 20px; + font-size: var(--font-size-regular); + } + } + + .process-container { + display: flex; + flex-direction: column; + + .throughput-value { + font-size: 1rem; + + .value { + font-weight: bold; + font-size: 1.5rem; + } + } + + .progress-bar-wrapper { + display: flex; + gap: 8px; + margin-top: 6px; + } + + .progress-bar { + position: relative; + width: 100%; + height: 4px; + border-radius: 13px; + overflow: hidden; + background: #fbebd7; + + .bar-fill { + position: absolute; + height: 100%; + top: 0; + left: 0; + background: #fc9d2f; + border-radius: 13px; + } + + .bar-fill.full { + width: 100%; + } + + .bar-fill.partial { + width: 0; // inline style will override this + } + } + } + + .metrics-section { + padding-top: 16px; + border-top: 1px solid var(--background-color-gray); + + .metric { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + margin-bottom: 8px; + + .label { + color: var(--text-color); + } + + .value { + font-weight: bold; + } + } + } + } + .throughoutSummary-wrapper { + .process-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 16px; + width: 100%; + + .throughput-value { + font-size: var(--font-size-small); + flex: 1; + display: flex; + flex-direction: column; + + .value { + color: var(--accent-color); + } + + /* Let the text take available space */ + } + + .lineChart { + max-width: 200px; + height: 100px; + position: relative; + + .assetUsage { + text-align: right; + position: absolute; + right: 0; + top: 0; + } + + canvas { + background: transparent; + } + } + } + + .footer { + display: flex; + gap: 16px; // Space between cards + margin-top: 24px; + + .footer-card { + width: 100%; + background: var(--background-color-gray); + border-radius: 6px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; + + &:first-child { + width: 85%; + } + + .header { + font-size: var(--font-size-regular); + } + + .value-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: end; + gap: 6px; + } + } + + .shiftUtilization { + .value-container { + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + + .value { + font-size: var(--font-size-xlarge); + } + + .progress-wrapper { + width: 100%; + display: flex; + gap: 6px; + + .progress { + border-radius: 6px; + height: 5px; + + &:nth-child(1) { + background: #f3c64d; + } + + &:nth-child(2) { + background: #67b3f4; + } + + &:nth-child(3) { + background: #7981f5; + } + } + } + + .progress-indicator { + display: flex; + justify-content: space-between; + width: 100%; + gap: 6px; + + .shift-wrapper { + display: flex; + align-items: center; + gap: 5px; + + /* Align items vertically */ + &:nth-child(1) { + .indicator { + background: #f3c64d; + } + } + + &:nth-child(2) { + .indicator { + background: #67b3f4; + } + } + + &:nth-child(3) { + .indicator { + background: #7981f5; + } + } + + label { + font-size: var(--font-size-small); + position: relative; + } + + .indicator { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + } + } + } + } + } + } + } + } + .roiSummary-wrapper { + max-width: 470px; + background-color: var(--background-color); + + .product-info { + display: flex; + } + + .playBack { + display: flex; + background-color: var(--background-color); + border-radius: 12px; + padding: 6px; + + .info { + span { + font-size: var(--font-size-xlarge); + + &:first-child { + color: #31c756; + } + + &:last-child { + color: var(--text-color); + } + } + } + } + + .roi-details { + display: flex; + align-items: center; + gap: 12px; + + .progress-wrapper { + width: 250px; + display: flex; + flex-direction: column; + gap: 6px; + + .content { + display: flex; + flex-direction: column; + gap: 3px; + align-items: center; + + .key { + font-size: var(--font-size-xlarge); + color: var(--accent-color); + } + } + } + + .roi-progress { + width: 100%; + } + + .metrics { + display: flex; + flex-direction: column; + gap: 6px; + + .metric-item { + width: 100%; + border-radius: #{$border-radius-xxx}; + border: 1px solid #00ff56; + background: #17eb5e42; + display: flex; + flex-direction: column; + padding: 4px 8px; + + &:last-child { + align-items: center; + } + + .metric-label { + opacity: 0.8; + font-size: 10px; + font-weight: 300; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .metric-value { + text-align: center; + line-height: 20px; + } + } + + .metric-wrapper { + display: flex; + gap: 6px; + + .metric-item { + border-radius: #{$border-radius-large}; + background-color: var(--background-color); + border: 1px solid var(--border-color); + } + } + } + } + + .cost-breakdown { + background-color: var(--background-color); + border: 1px solid var(--border-color); + border-radius: #{$border-radius-extra-large}; + padding: 16px; + + .breakdown-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + + .section-wrapper { + display: flex; + gap: 4px; + align-items: center; + } + + .section-number { + color: #00aaff; + } + + .section-title { + font-size: var(--font-size-regular); + color: var(--text-color); + } + + .expand-icon { + font-size: 16px; + color: var(--text-color); + cursor: pointer; + transform: rotate(90deg); + transition: transform 0.2s linear; + } + + .expand-icon.open { + transform: rotate(0deg); + } + } + + .breakdown-table { + width: 100%; + border-collapse: collapse; + border-radius: 8px; + overflow: hidden; + outline: 1px solid var(--border-color); + outline-offset: -1px; + margin-top: 12px; + + th, + td { + color: var(--text-color); + padding: 8px; + text-align: left; + border: 1px solid var(--border-color); + } + th { + background-color: var(--background-color); + } + } + } + + .tips-section { + background-color: var(--background-color); + border-radius: #{$border-radius-large}; + outline: 1px solid var(--border-color); + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px; + + .tip-header { + display: flex; + align-items: center; + + .tip-title { + color: var(--text-color); + font-weight: 600; + } + } + + .tip-description { + span { + font-size: var(--font-size-xlarge); + color: #34c759; + + &:first-child { + color: #34c759; + } + + &:nth-child(2) { + color: #488ef6; + } + } + } + } + + .get-tips-button { + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 14px; + margin-top: 8px; + display: inline-block; + display: flex; + justify-content: flex-end; + background: none; + + .btn { + color: var(--text-button-color); + background: var(--background-color-button); + padding: 4px 12px; + border-radius: #{$border-radius-large}; + display: inline-block; + text-align: center; + } + } + } + + .semi-circle-wrapper { + width: 100%; + height: 125px; + overflow-y: hidden; + position: relative; + .semi-circle { + width: 100%; + height: 250px; + border-radius: 50%; + position: relative; + } + .progress-cover { + position: absolute; + width: 75%; + height: 75%; + top: 12.5%; + left: 12.5%; + border-radius: 50%; + } + } + + .label-wrapper { + .label { + font-size: var(--font-size-xxxlarge); + } + + position: absolute; + bottom: 0%; + left: 50%; + transform: translate(-50%, 0%); + font-weight: bold; + font-size: 1.2rem; + color: #333; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } +} + +.breakdown-table-wrapper { + &.closed { + max-height: 0; + padding: 0; + } + + &.open { + max-height: 500px; + } + + .breakdown-table { + width: 100%; + border-collapse: collapse; + + th, + td { + padding: 10px; + border: 1px solid #ddd; + text-align: left; + } + } +} + +// Breakdown Table Open/Close Logic + diff --git a/app/src/styles/components/simulation/simulation.scss b/app/src/styles/components/simulation/simulation.scss index 0fc3df8..88f85dc 100644 --- a/app/src/styles/components/simulation/simulation.scss +++ b/app/src/styles/components/simulation/simulation.scss @@ -32,10 +32,17 @@ } .controls-container { - @include flex-center; + @include flex-space-between; gap: 12px; justify-content: space-between; - + .header{ + @include flex-center; + gap: 6px; + padding: 0 8px; + svg{ + scale: 1.3; + } + } .production-details, .controls-wrapper { @include flex-center; @@ -72,7 +79,7 @@ .expand-icon-container { @include flex-center; - padding: 6px 8px; + padding: 0 8px; cursor: pointer; } @@ -307,6 +314,22 @@ font-size: var(--font-size-tiny); } + .timmer { + width: auto; + position: absolute; + bottom: 0; + font-size: var(--font-size-tiny); + } + + .start-displayer { + left: 8px; + } + + .end-displayer { + width: auto; + right: 8px; + } + .start-displayer { bottom: 4px; left: 16px; @@ -347,6 +370,12 @@ } .simulation-player-container.open { + + .start-displayer, + .end-displayer { + display: none; + } + .timmer { display: none; } diff --git a/app/src/styles/components/tools.scss b/app/src/styles/components/tools.scss index e476c3c..29d37b4 100644 --- a/app/src/styles/components/tools.scss +++ b/app/src/styles/components/tools.scss @@ -15,7 +15,7 @@ transition: width 0.2s; background: var(--background-color); backdrop-filter: blur(8px); - z-index: #{$z-index-default}; + z-index: 2; outline: 1px solid var(--border-color); outline-offset: -1px; @@ -124,6 +124,8 @@ padding: 4px; border-radius: #{$border-radius-medium}; background: var(--background-color); + outline: 1px solid var(--border-color); + outline-offset: -1px; gap: 5px; position: relative; diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index c7f1694..f8c022f 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -58,12 +58,8 @@ fill: var(--icon-default-color-active); } &:hover { - rect { - stroke: var(--icon-default-color); - } - circle { - fill: var(--icon-default-color); - } + filter: saturate(0.8); + background: var(--background-color-accent); } } } @@ -420,7 +416,7 @@ outline: none; path { stroke: var(--text-button-color); - stroke-width: 1.3; + strokeWidth: 1.3; } } } @@ -686,7 +682,7 @@ path { stroke: var(--accent-color); - stroke-width: 1.5px; + strokeWidth: 1.5px; } &:hover { @@ -714,10 +710,10 @@ .add-button { @include flex-center; - padding: 2px 4px; + padding: 4px 8px; background: var(--background-color-button); color: var(--text-button-color); - border-radius: #{$border-radius-small}; + border-radius: #{$border-radius-large}; cursor: pointer; outline: none; border: none; @@ -832,10 +828,10 @@ transform: translateX(4px); &:hover { - background: var(--accent-color); + background: var(--background-color-accent); path { - stroke: var(--primary-color); + stroke: var(--text-button-color); } } } @@ -1003,10 +999,10 @@ border-radius: 8px 0 0 8px; &:hover { - background: var(--accent-color); + background: var(--background-color-accent); path { - stroke: var(--primary-color); + stroke: var(--text-button-color); } } } @@ -1067,7 +1063,13 @@ .dropdown-content-container { padding: 6px 12px; } - + .value-field-container { + padding: 6px; + .dropdown { + min-width: 44px; + text-align: center; + } + } .input-range-container { .input-container { width: 75%; diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss index 66a60e7..3adfc0f 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -13,7 +13,6 @@ @use 'components/button'; @use 'components/form'; @use 'components/input'; -@use 'components/layouts'; @use 'components/lists'; @use 'components/moduleToggle'; @use 'components/templates'; @@ -22,11 +21,12 @@ @use 'components/visualization/ui/styledWidgets'; @use 'components/visualization/floating/common'; @use 'components/marketPlace/marketPlace'; -@use 'components/simulation/simulation'; @use 'components/menu/menu'; @use 'components/confirmationPopUp'; -@use 'components/analysis/analysis'; -@use 'components/analysis/ROISummary.scss'; +@use 'components/simulation/simulation'; +@use 'components/simulation/analysis'; +@use 'components/logs/logs'; +@use 'components/footer/footer.scss'; // layout @use 'layout/loading'; diff --git a/app/src/styles/pages/realTimeViz.scss b/app/src/styles/pages/realTimeViz.scss index fa1b86c..abc626a 100644 --- a/app/src/styles/pages/realTimeViz.scss +++ b/app/src/styles/pages/realTimeViz.scss @@ -3,8 +3,6 @@ // Main Container .realTime-viz { - background: #131313; - box-shadow: $box-shadow-medium; width: calc(100% - (320px + 270px + 90px)); height: calc(100% - (250px)); position: absolute; @@ -12,8 +10,8 @@ left: calc(270px + 45px); transform: translate(0, -50%); border-radius: #{$border-radius-medium}; - transition: all 0.2s; - z-index: #{$z-index-default}; + z-index: 2; + pointer-events: none; .realTime-viz-wrapper { width: 100%; @@ -39,10 +37,6 @@ z-index: 1; } - .scene-container { - overflow: hidden; - } - .icon { display: flex; align-items: center; @@ -74,6 +68,8 @@ z-index: 3; transform: translate(-50%, -10%); transition: transform 0.5s linear; + pointer-events: all; + &::-webkit-scrollbar { display: none; } @@ -367,6 +363,7 @@ border-radius: 2px; transition: transform 0.3s ease; box-shadow: #{$box-shadow-medium}; + pointer-events: all; svg { stroke: var(--icon-default-color) !important; @@ -426,10 +423,8 @@ path { stroke: #f65648; - stroke-width: 1.3; + strokeWidth: 1.3; } - - } } @@ -778,17 +773,10 @@ } } - - - .panel-content { background: var(--background-color); - } - - - /* RIGHT */ .panel-content.right-opening { animation: rightExpand 0.5s ease-in-out forwards; @@ -913,9 +901,6 @@ } } - - - // Add button .extra-Bs-addopening { @@ -926,7 +911,6 @@ animation: slideUp 0.3s ease forwards; } - @keyframes slideDown { from { opacity: 0; diff --git a/app/src/types/simulationTypes.d.ts b/app/src/types/simulationTypes.d.ts index cb71864..11d5156 100644 --- a/app/src/types/simulationTypes.d.ts +++ b/app/src/types/simulationTypes.d.ts @@ -13,7 +13,7 @@ interface TriggerSchema { delay: number; triggeredAsset: { triggeredModel: { modelName: string, modelUuid: string }; - triggeredPoint: { pointName: string, pointUuid: string }; + triggeredPoint: { pointName: string, pointUuid: string } | null; triggeredAction: { actionName: string, actionUuid: string } | null; } | null; } @@ -88,6 +88,7 @@ interface StoragePointSchema { actionType: "store"; materials: { materialName: string; materialId: string; }[]; storageCapacity: number; + triggers: TriggerSchema[]; }; } @@ -143,6 +144,10 @@ interface MachineStatus extends MachineEventSchema { isActive: boolean; idleTime: number; activeTime: number; + currentAction?: { + actionUuid: string; + actionName: string; + }; } interface ArmBotStatus extends RoboticArmEventSchema { diff --git a/app/src/utils/handleSnap.ts b/app/src/utils/handleSnap.ts new file mode 100644 index 0000000..bd6a74d --- /dev/null +++ b/app/src/utils/handleSnap.ts @@ -0,0 +1,22 @@ +export function snapControls(value: number, event: string): number { + const CTRL_DISTANCE = 1; // Snap to whole numbers when Ctrl is pressed + const SHIFT_DISTANCE = 0.01; // Snap to half-step increments when Shift is pressed + const CTRL_SHIFT_DISTANCE = 0.1; // Snap to fine increments when both Ctrl and Shift are pressed + + switch (event) { + case "Ctrl": + return Math.round(value / CTRL_DISTANCE) * CTRL_DISTANCE; + + case "Shift": + return Math.round(value / SHIFT_DISTANCE) * SHIFT_DISTANCE; + + case "Ctrl+Shift": + const base = Math.floor(value / CTRL_DISTANCE) * CTRL_DISTANCE; + const offset = + Math.round((value - base) / CTRL_SHIFT_DISTANCE) * CTRL_SHIFT_DISTANCE; + return base + offset; + + default: + return value; // No snapping if no modifier key is pressed + } +}