v2 #74

Merged
Vishnu merged 46 commits from v2 into main 2025-05-03 04:44:41 +00:00
80 changed files with 4579 additions and 2342 deletions

View File

@ -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 (
<ToastProvider>
<LoggerProvider>
<Router>
<Routes>
<Route
path="/"
element={<UserAuth />}
/>
<Route path="/" element={<UserAuth />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/project" element={<Project />} />
</Routes>
</Router>
</ToastProvider>
</LoggerProvider>
);
};

View File

@ -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 <LogInfoIcon />;
case "error":
return <ErrorIcon />;
case "warning":
return <WarningIcon />;
case "log":
default:
return <LogInfoIcon />;
}
};
const Footer: React.FC = () => {
const { logs, setIsLogListVisible } = useLogger();
const lastLog = logs.length > 0 ? logs[logs.length - 1] : null;
return (
<div className="footer-wrapper">
<div className="selection-wrapper">
<div className="selector-wrapper">
<div className="icon"></div>
<div className="selector">Selection</div>
</div>
<div className="selector-wrapper">
<div className="icon"></div>
<div className="selector">Rotate/Zoom</div>
</div>
<div className="selector-wrapper">
<div className="icon"></div>
<div className="selector">Pan/Context Menu</div>
</div>
</div>
<div className="logs-wrapper">
<div className="logs-detail" onClick={() => setIsLogListVisible(true)}>
{lastLog ? (
<>
<span className="log-icon">{getLogIcon(lastLog.type)}</span>
<span className="log-message">{lastLog.message}</span>
</>
) : (
"No logs yet."
)}
</div>
<div className="version">
V 0.01
<div className="icon">
<HelpIcon />
</div>
</div>
</div>
</div>
);
};
export default Footer;

View File

@ -458,7 +458,7 @@ export function InfoIcon() {
>
<path
d="M5.46289 7.75441C5.46289 7.68342 5.47691 7.6131 5.50422 7.54758C5.53158 7.48201 5.57156 7.42254 5.62201 7.37257C5.67246 7.3226 5.73231 7.2831 5.79807 7.25636C5.86388 7.22963 5.93425 7.21619 6.00529 7.21681C6.11003 7.21873 6.21188 7.25142 6.29814 7.31089C6.38435 7.37036 6.45121 7.45398 6.49019 7.55118C6.52921 7.64843 6.53862 7.75499 6.5174 7.85757C6.49614 7.96014 6.44511 8.05417 6.37071 8.12795C6.29631 8.20168 6.20185 8.25184 6.09908 8.27219C5.99631 8.29254 5.8898 8.28212 5.79294 8.24224C5.69603 8.2024 5.61308 8.13486 5.55438 8.04808C5.49567 7.96134 5.46385 7.8592 5.46289 7.75441ZM5.63564 6.44401L5.56844 3.93842C5.56206 3.87819 5.56844 3.81729 5.58716 3.75968C5.60583 3.70207 5.63641 3.64902 5.67692 3.604C5.71743 3.55897 5.76697 3.52297 5.82227 3.49832C5.87761 3.47368 5.93751 3.46094 5.99804 3.46094C6.05862 3.46094 6.11852 3.47368 6.17387 3.49832C6.22916 3.52297 6.2787 3.55897 6.31921 3.604C6.35972 3.64902 6.3903 3.70207 6.40897 3.75968C6.42769 3.81729 6.43407 3.87819 6.42769 3.93842L6.36529 6.44401C6.36529 6.54073 6.32689 6.63356 6.25844 6.70196C6.19004 6.77036 6.09721 6.80881 6.00049 6.80881C5.90372 6.80881 5.81094 6.77036 5.74254 6.70196C5.67414 6.63356 5.63564 6.54073 5.63564 6.44401Z"
fill="var(--icon-default-color)"
fill="var(--text-color)"
/>
<path
d="M6.00006 10.3175C8.45219 10.3175 10.4401 8.32963 10.4401 5.8775C10.4401 3.42536 8.45219 1.4375 6.00006 1.4375C3.54792 1.4375 1.56006 3.42536 1.56006 5.8775C1.56006 8.32963 3.54792 10.3175 6.00006 10.3175Z"
@ -471,7 +471,7 @@ export function InfoIcon() {
);
}
export function AI_Icon() {
export function AIIcon() {
return (
<svg
width="20"
@ -814,3 +814,136 @@ export const SpeedIcon = () => {
</svg>
);
};
export const LogListIcon = () => {
return (
<svg
width="24"
height="25"
viewBox="0 0 24 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5 18.25H5.5M18.5 21.7272V19.3568H17.3829C16.8245 19.3568 16.375 18.9072 16.375 18.3489V18.08C16.375 17.5216 16.8246 17.0721 17.3829 17.0721H18.5V13.7318H17.2579C16.6995 13.7318 16.25 13.2822 16.25 12.7239V12.4551C16.25 11.8967 16.6996 11.4472 17.2579 11.4472H18.5V7.98185H17.2579C16.6995 7.98185 16.25 7.5323 16.25 6.97395V6.70515C16.25 6.14675 16.6996 5.69725 17.2579 5.69725H18.5V3.27065M14.5 12.75H5.5M12 7.24995H5.5M20.25 3.25H3.75C3.1977 3.25 2.75 3.6977 2.75 4.25V20.75C2.75 21.3023 3.1977 21.75 3.75 21.75H20.25C20.8023 21.75 21.25 21.3023 21.25 20.75V4.25C21.25 3.6977 20.8023 3.25 20.25 3.25Z"
stroke="#2B3344"
strokeWidth="1.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};
export const LogTickIcon = () => {
return (
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_3962_4223)">
<path
d="M5.00065 0.835938C2.70482 0.835938 0.833984 2.70677 0.833984 5.0026C0.833984 7.29844 2.70482 9.16927 5.00065 9.16927C7.29648 9.16927 9.16732 7.29844 9.16732 5.0026C9.16732 2.70677 7.29648 0.835938 5.00065 0.835938ZM6.99232 4.04427L4.62982 6.40677C4.57148 6.4651 4.49232 6.49844 4.40898 6.49844C4.32565 6.49844 4.24648 6.4651 4.18815 6.40677L3.00898 5.2276C2.88815 5.10677 2.88815 4.90677 3.00898 4.78594C3.12982 4.6651 3.32982 4.6651 3.45065 4.78594L4.40898 5.74427L6.55065 3.6026C6.67148 3.48177 6.87148 3.48177 6.99232 3.6026C7.11315 3.72344 7.11315 3.91927 6.99232 4.04427Z"
fill="#49B841"
/>
</g>
<defs>
<clipPath id="clip0_3962_4223">
<rect width="10" height="10" fill="white" />
</clipPath>
</defs>
</svg>
);
};
export const LogInfoIcon = () => {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.75 2.64438V7.41438C0.75 7.92094 0.951232 8.40675 1.30943 8.76495C1.66762 9.12314 2.15344 9.32438 2.66 9.32438H3.615V10.7544L6.5 9.32438H9.365C9.87156 9.32438 10.3574 9.12314 10.7156 8.76495C11.0738 8.40675 11.275 7.92094 11.275 7.41438V2.64438C11.275 2.13781 11.0738 1.652 10.7156 1.2938C10.3574 0.935607 9.87156 0.734375 9.365 0.734375H2.66C2.15344 0.734375 1.66762 0.935607 1.30943 1.2938C0.951232 1.652 0.75 2.13781 0.75 2.64438Z"
fill="#8E8E93"
/>
<path
d="M5.04492 6.94531H6.95492"
stroke="white"
strokeWidth="0.955"
strokeMiterlimit="10"
/>
<path
d="M5.04492 4.07812H5.99992V6.94313"
stroke="white"
strokeWidth="0.955"
strokeMiterlimit="10"
/>
<path
d="M5.52539 2.65625H6.47539"
stroke="white"
strokeWidth="0.955"
strokeMiterlimit="10"
/>
</svg>
);
};
export const WarningIcon = () => {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.75 2.64438V7.41438C0.75 7.92094 0.951232 8.40675 1.30943 8.76495C1.66762 9.12314 2.15344 9.32438 2.66 9.32438H3.615V10.7544L6.5 9.32438H9.365C9.87156 9.32438 10.3574 9.12314 10.7156 8.76495C11.0738 8.40675 11.275 7.92094 11.275 7.41438V2.64438C11.275 2.13781 11.0738 1.652 10.7156 1.2938C10.3574 0.935607 9.87156 0.734375 9.365 0.734375H2.66C2.15344 0.734375 1.66762 0.935607 1.30943 1.2938C0.951232 1.652 0.75 2.13781 0.75 2.64438Z"
fill="#8E8E93"
/>
<path
d="M5.04492 6.94531H6.95492"
stroke="white"
strokeWidth="0.955"
strokeMiterlimit="10"
/>
<path
d="M5.04492 4.07812H5.99992V6.94313"
stroke="white"
strokeWidth="0.955"
strokeMiterlimit="10"
/>
<path
d="M5.52539 2.65625H6.47539"
stroke="white"
strokeWidth="0.955"
strokeMiterlimit="10"
/>
</svg>
);
};
export const ErrorIcon = () => {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11 6C11 8.7614 8.7614 11 6 11C3.23857 11 1 8.7614 1 6C1 3.23857 3.23857 1 6 1C8.7614 1 11 3.23857 11 6ZM4.48482 4.48483C4.63126 4.33838 4.8687 4.33838 5.01515 4.48483L6 5.46965L6.9848 4.48484C7.13125 4.33839 7.3687 4.33839 7.51515 4.48484C7.6616 4.63128 7.6616 4.86872 7.51515 5.01515L6.5303 6L7.51515 6.9848C7.6616 7.13125 7.6616 7.3687 7.51515 7.51515C7.3687 7.6616 7.13125 7.6616 6.9848 7.51515L6 6.53035L5.01515 7.51515C4.86871 7.6616 4.63127 7.6616 4.48483 7.51515C4.33838 7.3687 4.33838 7.13125 4.48483 6.98485L5.46965 6L4.48482 5.01515C4.33837 4.86871 4.33837 4.63127 4.48482 4.48483Z"
fill="#ED5342"
/>
</svg>
);
};

View File

@ -125,21 +125,21 @@ export function ResetIcon() {
>
<path
d="M2.54182 4.09637C3.33333 2.73422 4.80832 1.81836 6.49721 1.81836C9.02194 1.81836 11.0686 3.86506 11.0686 6.38979C11.0686 8.91452 9.02194 10.9612 6.49721 10.9612C3.97248 10.9612 1.92578 8.91452 1.92578 6.38979"
stroke="var(--icon-default-color-active)"
stroke="var(--text-color)"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M7.64118 6.38895C7.64118 7.02013 7.12951 7.53181 6.49833 7.53181C5.86714 7.53181 5.35547 7.02013 5.35547 6.38895C5.35547 5.75777 5.86714 5.24609 6.49833 5.24609C7.12951 5.24609 7.64118 5.75777 7.64118 6.38895Z"
fill="var(--icon-default-color-active)"
stroke="var(--icon-default-color-active)"
fill="var(--text-color)"
stroke="var(--text-color)"
strokeWidth="0.571429"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.78125 4.10407H2.49554V1.81836"
stroke="var(--icon-default-color-active)"
stroke="var(--text-color)"
strokeLinecap="round"
strokeLinejoin="round"
/>
@ -158,7 +158,7 @@ export function PlayStopIcon() {
>
<path
d="M8 2.88867V9.88867M11 2.88867V9.88867M2 3.99171V8.78562C2 9.28847 2 9.53987 2.09943 9.65627C2.1857 9.75732 2.31512 9.81092 2.44756 9.80047C2.60019 9.78847 2.77796 9.61072 3.13352 9.25517L5.5305 6.85817C5.69485 6.69382 5.777 6.61167 5.8078 6.51692C5.83485 6.43357 5.83485 6.34377 5.8078 6.26042C5.777 6.16567 5.69485 6.08352 5.5305 5.91917L3.13352 3.52219C2.77796 3.16664 2.60019 2.98886 2.44756 2.97685C2.31512 2.96643 2.1857 3.02004 2.09943 3.12105C2 3.23747 2 3.48888 2 3.99171Z"
stroke="var(--icon-default-color-active)"
stroke="var(--text-color)"
strokeLinecap="round"
strokeLinejoin="round"
/>
@ -177,7 +177,7 @@ export function ExitIcon() {
>
<path
d="M2.08203 6.34311L5.03647 3.38867M2.08203 6.34311L5.03647 9.29755M2.08203 6.34311H5.9228C7.22276 6.34334 9.34995 7.05241 10.7681 9.88867"
stroke="var(--icon-default-color-active)"
stroke="var(--text-color)"
strokeLinecap="round"
strokeLinejoin="round"
/>

View File

@ -19,7 +19,7 @@ const Header: React.FC = () => {
<FileMenu />
</div>
</div>
<div
<button
className={`toggle-sidebar-ui-button ${!toggleUI ? "active" : ""}`}
onClick={() => {
if (activeModule !== "market") {
@ -29,7 +29,7 @@ const Header: React.FC = () => {
}}
>
<ToggleSidebarIcon />
</div>
</button>
</div>
);
};

View File

@ -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 = () => {
<div className="analysis-main-container">
<div className="header">Object</div>
<div className="generate-report-button">
<AI_Icon /> Generate Report
<AIIcon /> Generate Report
</div>
<div className="analysis-content-container section">
<div className="dropdown-header-container">
@ -121,7 +91,7 @@ const Analysis: React.FC = () => {
/>
<div className="buttons-container">
<input type="button" value={"Clear"} className="cancel" />
<input type="button" value={"Calculate"} className="submit" />
<input type="button" value={"Update"} className="submit" />
</div>
<div className="create-custom-analysis-container">
<div className="custom-analysis-header">Create Custom Analysis</div>

View File

@ -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 = () => {
<section>
<div className="header">Environment</div>
<div className="optimize-button" onClick={optimizeScene}>
<AI_Icon />
<AIIcon />
Optimize
</div>

View File

@ -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<EventsSchema | null>(null);
const [assetType, setAssetType] = useState<string | null>(null);
const { products, addEvent } = useProductStore();
const { selectedEventSphere } = useSelectedEventSphere();
const { selectedEventData } = useSelectedEventData();
const { getEventByModelUuid } = useProductStore();
const { selectedProduct } = useSelectedProduct();
const [currentEventData, setCurrentEventData] = useState<EventsSchema | null>(
null
);
const [assetType, setAssetType] = useState<string | null>(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 (
<div className="event-proprties-wrapper">
{currentEventData && (
<>
<div className="header">
<div className="header-value">
{selectedEventData?.data.modelName}
</div>
</div>
{assetType === "conveyor" && <ConveyorMechanics />}
{assetType === "vehicle" && <VehicleMechanics />}
{assetType === "roboticArm" && <RoboticArmMechanics />}
{assetType === "machine" && <MachineMechanics />}
{assetType === "storageUnit" && <StorageMechanics />}
</>
)}
{!currentEventData && selectedEventSphere && (
<div className="no-event-selected">
<p>
<strong>Oops!</strong> It looks like this object doesn't have an
event assigned yet. To continue, please link it to one of the
products below.
</p>
<div className="products-list">
<p>
<strong>Here are some products you can add it to:</strong>
</p>
<ul>
{products.map((product) => (
<li key={product.productId}>
<button
onClick={() => {
if (selectedEventData) {
handleAddEventToProduct({
event: useEventsStore.getState().getEventByModelUuid(selectedEventData?.data.modelUuid),
addEvent,
selectedProduct,
})
}
}}
>
<AddIcon />
{product.productName}
</button>
</li>
))}
</ul>
</div>
</div>
)
}
{!selectedEventSphere && (
<div className="no-event-selected">
<p>
<strong>Oops!</strong> It looks like you haven't selected an event
point yet. Please select an event to view its properties.
</p>
</div>
)}
</div >
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 (
<div className="event-proprties-wrapper">
{currentEventData && (
<>
<div className="header">
<div className="header-value">
{selectedEventData?.data.modelName}
</div>
</div>
{assetType === "conveyor" && <ConveyorMechanics />}
{assetType === "vehicle" && <VehicleMechanics />}
{assetType === "roboticArm" && <RoboticArmMechanics />}
{assetType === "machine" && <MachineMechanics />}
{assetType === "storageUnit" && <StorageMechanics />}
</>
)}
{!currentEventData && selectedEventSphere && (
<div className="no-event-selected">
<p>
<strong>Oops!</strong> It looks like this object doesn't have an
event assigned yet. To continue, please link it to one of the
products below.
</p>
<div className="products-list">
<p>
<strong>Here are some products you can add it to:</strong>
</p>
<div className="product-item">
{products.map((product) => (
<button
key={product.productId}
onClick={() => {
if (selectedEventData) {
handleAddEventToProduct({
event: useEventsStore
.getState()
.getEventByModelUuid(
selectedEventData?.data.modelUuid
),
addEvent,
selectedProduct,
});
}
}}
>
<AddIcon />
{product.productName}
</button>
))}
</div>
</div>
</div>
)}
{!selectedEventSphere && (
<div className="no-event-selected">
<p>
<strong>Oops!</strong> It looks like you haven't selected an event
point yet. Please select an event to view its properties.
</p>
</div>
)}
</div>
);
};
export default EventProperties;

View File

@ -35,7 +35,7 @@ const ActionsList: React.FC<ActionsListProps> = ({
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({

View File

@ -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() {
</div>
</div>
<div className="tirgger">
<Trigger />
<Trigger selectedPointData={selectedPointData as any} type= {'Conveyor'}/>
</div>
</section>
</>

View File

@ -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() {
</div>
</div>
<div className="tirgger">
<Trigger />
<Trigger selectedPointData={selectedPointData as any} type= {'Machine'} />
</div>
</section>
)}

View File

@ -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() {
/>
</div>
<div className="tirgger">
<Trigger />
<Trigger selectedPointData={selectedPointData as any} type= {'RoboticArm'} />
</div>
</div>
)}

View File

@ -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() {
</div>
</div>
<div className="tirgger">
<Trigger />
<Trigger selectedPointData={selectedPointData as any} type= {'StorageUnit'} />
</div>
</section>
)}

View File

@ -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() {
</div>
</div>
<div className="tirgger">
<Trigger />
<Trigger selectedPointData={selectedPointData as any} type= {'Vehicle'} />
</div>
</section>
</>

View File

@ -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<string[]>(["Trigger 1"]);
const [selectedTrigger, setSelectedTrigger] = useState<string>("Trigger 1");
const [activeOption, setActiveOption] = useState("onComplete");
const triggersContainerRef = useRef<HTMLDivElement>(null);
type TriggerProps = {
selectedPointData?: PointsScheme | undefined;
type?: 'Conveyor' | 'Vehicle' | 'RoboticArm' | 'Machine' | 'StorageUnit';
};
// States for dropdowns
const [triggeredModel, setTriggeredModel] = useState<string[]>([]);
const [triggeredPoint, setTriggeredPoint] = useState<string[]>([]);
const [triggeredAction, setTriggeredAction] = useState<string[]>([]);
const Trigger = ({ selectedPointData, type }: TriggerProps) => {
const [currentAction, setCurrentAction] = useState<string | undefined>();
const { selectedProduct } = useSelectedProduct();
const { getActionByUuid, addTrigger, removeTrigger, updateTrigger, renameTrigger, getProductById } = useProductStore();
const [triggers, setTriggers] = useState<TriggerSchema[]>([]);
const [selectedTrigger, setSelectedTrigger] = useState<TriggerSchema | undefined>();
const [activeOption, setActiveOption] = useState<"onComplete" | "onStart" | "onStop" | "delay" | "onError">("onComplete");
const triggersContainerRef = useRef<HTMLDivElement>(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 (
<div className="trigger-wrapper">
<div className="header">
<div className="title">Trigger</div>
<button
className="add-button"
onClick={addTrigger}
style={{ cursor: "pointer" }}
>
<AddIcon /> Add
</button>
</div>
<div className="trigger-list">
<div
className="lists-main-container"
ref={triggersContainerRef}
style={{ height: "120px" }}
>
<div className="list-container">
{triggers.map((trigger: any, index: number) => (
<div
key={index}
className={`list-item ${
selectedTrigger === trigger ? "active" : ""
}`}
onClick={() => setSelectedTrigger(trigger)}
>
<button className="value" onClick={() => {}}>
<RenameInput value={trigger} onRename={() => {}} />
if (type === 'Conveyor' || type === 'Vehicle' || type === 'Machine' || type === 'StorageUnit') {
actionUuid = (selectedPointData as ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema).action?.actionUuid;
} else if (type === 'RoboticArm') {
actionUuid = (selectedPointData as RoboticArmPointSchema).actions[0]?.actionUuid;
}
setCurrentAction(actionUuid);
}, [selectedPointData, selectedProduct, type]);
const updateBackend = (
productName: string,
productId: string,
organization: string,
eventData: EventsSchema
) => {
upsertProductOrEventApi({
productName: productName,
productId: productId,
organization: organization,
eventDatas: eventData
})
}
useEffect(() => {
if (!currentAction || !selectedProduct) return;
const action = getActionByUuid(selectedProduct.productId, currentAction);
const actionTriggers = action?.triggers || [];
setTriggers(actionTriggers);
setSelectedTrigger(actionTriggers[0]);
}, [currentAction, selectedProduct]);
const handleAddTrigger = () => {
if (!selectedProduct || !currentAction) return;
const newTrigger: TriggerSchema = {
triggerUuid: THREE.MathUtils.generateUUID(),
triggerName: `New Trigger ${triggers.length + 1}`,
triggerType: activeOption,
delay: 0,
triggeredAsset: null
};
addTrigger(selectedProduct.productId, currentAction, newTrigger);
setSelectedTrigger(newTrigger);
};
const handleRemoveTrigger = (triggerUuid: string) => {
if (!selectedProduct) return;
removeTrigger(selectedProduct.productId, triggerUuid);
if (selectedTrigger?.triggerUuid === triggerUuid) {
const remainingTriggers = triggers.filter(t => t.triggerUuid !== triggerUuid);
setSelectedTrigger(remainingTriggers[0]);
}
};
const handleTriggerRename = (triggerUuid: string, newName: string) => {
if (!selectedProduct) return;
renameTrigger(selectedProduct.productId, triggerUuid, newName);
};
const handleTriggerTypeChange = (option: string) => {
if (!selectedTrigger || !selectedProduct) return;
const validTypes: Array<TriggerSchema['triggerType']> = ["onComplete", "onStart", "onStop", "delay", "onError"];
if (!validTypes.includes(option as TriggerSchema['triggerType'])) return;
setActiveOption(option as TriggerSchema['triggerType']);
updateTrigger(selectedProduct.productId, selectedTrigger.triggerUuid, {
triggerType: option as TriggerSchema['triggerType']
});
};
const triggeredModel = selectedTrigger?.triggeredAsset?.triggeredModel || { modelName: "Select Model", modelUuid: "" };
const triggeredPoint = selectedTrigger?.triggeredAsset?.triggeredPoint || { pointName: "Select Point", pointUuid: "" };
const triggeredAction = selectedTrigger?.triggeredAsset?.triggeredAction || { actionName: "Select Action", actionUuid: "" };
console.log('selectedTrigger: ', selectedTrigger);
console.log('triggeredAction: ', triggeredAction);
const modelOptions = getProductById(selectedProduct.productId)?.eventDatas || [];
const pointOptions: PointsScheme[] = useMemo(() => {
if (!triggeredModel.modelUuid) return [];
const model = modelOptions.find(m => m.modelUuid === triggeredModel.modelUuid);
if (!model) return [];
if ('points' in model) {
return (model as ConveyorEventSchema).points;
} else if ('point' in model) {
return [(model as VehicleEventSchema | RoboticArmEventSchema | MachineEventSchema | StorageEventSchema).point];
}
return [];
}, [triggeredModel.modelUuid, modelOptions]);
const actionOptions: any = useMemo(() => {
if (!triggeredPoint.pointUuid) return [];
const point = pointOptions.find((p) => p.uuid === triggeredPoint.pointUuid);
if (!point) return [];
if ('action' in point) {
const typedPoint = point as ConveyorPointSchema | VehiclePointSchema | MachinePointSchema | StoragePointSchema;
return typedPoint.action ? [typedPoint.action] : [];
} else if ('actions' in point) {
const typedPoint = point as RoboticArmPointSchema;
return typedPoint.actions;
}
return [];
}, [triggeredPoint.pointUuid, pointOptions]);
const handleModelSelect = (option: string, triggerUuid: string) => {
if (!selectedProduct) return;
const selectedModel = modelOptions.find(m => m.modelName === option);
if (!selectedModel) return;
const event = updateTrigger(selectedProduct.productId, triggerUuid, {
triggeredAsset: {
triggeredModel: {
modelName: selectedModel.modelName,
modelUuid: selectedModel.modelUuid
},
triggeredPoint: null,
triggeredAction: null
}
});
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productId,
organization,
event
);
}
};
const handlePointSelect = (option: string, triggerUuid: string) => {
if (!selectedProduct || !selectedTrigger) return;
const pointUuid = pointOptions.find(p => `Point ${p.uuid.slice(0, 5)}` === option)?.uuid;
if (!pointUuid) return;
if (selectedTrigger.triggeredAsset?.triggeredModel) {
const event = updateTrigger(selectedProduct.productId, triggerUuid, {
triggeredAsset: {
...selectedTrigger.triggeredAsset,
triggeredPoint: {
pointName: option,
pointUuid: pointUuid
},
triggeredAction: null
}
});
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productId,
organization,
event
);
}
}
};
const handleActionSelect = (option: string, triggerUuid: string) => {
if (!selectedProduct || !selectedTrigger) return;
const selectedAction = actionOptions.find((a: any) => a.actionName === option);
if (!selectedAction) return;
if (selectedTrigger.triggeredAsset?.triggeredPoint) {
const event = updateTrigger(selectedProduct.productId, triggerUuid, {
triggeredAsset: {
...selectedTrigger.triggeredAsset,
triggeredAction: {
actionName: option,
actionUuid: selectedAction.actionUuid
}
}
});
if (event) {
updateBackend(
selectedProduct.productName,
selectedProduct.productId,
organization,
event
);
}
}
};
return (
<div className="trigger-wrapper">
<div className="header">
<div className="title">Trigger</div>
<button
className="add-button"
onClick={handleAddTrigger}
style={{ cursor: "pointer" }}
disabled={!currentAction}
>
<AddIcon /> Add
</button>
{triggers.length > 1 && (
<button
className="remove-button"
onClick={() => removeTrigger(index)}
>
<RemoveIcon />
</button>
</div>
<div className="trigger-list">
<div
className="lists-main-container"
ref={triggersContainerRef}
style={{ height: "120px" }}
>
<div className="list-container">
{triggers.map((trigger) => (
<div
key={trigger.triggerUuid}
className={`list-item ${selectedTrigger?.triggerUuid === trigger.triggerUuid ? "active" : ""}`}
onClick={() => setSelectedTrigger(trigger)}
>
<button className="value">
<RenameInput
value={trigger.triggerName}
onRename={(newName) => handleTriggerRename(trigger.triggerUuid, newName)}
/>
</button>
{triggers.length > 1 && (
<button
className="remove-button"
onClick={(e) => {
e.stopPropagation();
handleRemoveTrigger(trigger.triggerUuid);
}}
>
<RemoveIcon />
</button>
)}
</div>
))}
</div>
<button
className="resize-icon"
id="action-resize"
onMouseDown={(e: any) => handleResize(e, triggersContainerRef)}
>
<ResizeHeightIcon />
</button>
</div>
{selectedTrigger && (
<div className="trigger-item">
<div className="trigger-name">{selectedTrigger.triggerName}</div>
<LabledDropdown
label="Trigger Type"
defaultOption={selectedTrigger.triggerType}
options={["onComplete", "onStart", "onStop", "delay", "onError"]}
onSelect={handleTriggerTypeChange}
/>
<div className="trigger-options">
<LabledDropdown
label="Triggered Object"
defaultOption={triggeredModel.modelName}
options={[...modelOptions.map((option) => (option.modelName))]}
onSelect={(option) => { handleModelSelect(option, selectedTrigger.triggerUuid) }}
/>
<LabledDropdown
label="Triggered Point"
defaultOption={triggeredPoint.pointName}
options={[...pointOptions.map((option) => (`Point ${option.uuid.slice(0, 5)}`))]}
onSelect={(option) => { handlePointSelect(option, selectedTrigger.triggerUuid) }}
/>
<LabledDropdown
label="Triggered Action"
defaultOption={triggeredAction.actionName}
options={[...actionOptions.map((option: any) => (option.actionName))]}
onSelect={(option) => { handleActionSelect(option, selectedTrigger.triggerUuid) }}
/>
</div>
</div>
)}
</div>
))}
</div>
<button
className="resize-icon"
id="action-resize"
onMouseDown={(e: any) => handleResize(e, triggersContainerRef)}
>
<ResizeHeightIcon />
</button>
</div>
</div>
<div className="trigger-item">
<div className="trigger-name">{selectedTrigger}</div>
<LabledDropdown
label="Trigger on"
defaultOption={activeOption}
options={["onComplete", "onStart", "onStop", "delay"]}
onSelect={(option) => setActiveOption(option)}
/>
<div className="trigger-options">
<LabledDropdown
label="Triggered Object"
defaultOption={triggeredModel[0] || "Select Model"}
options={["Model 1", "Model 2", "Model 3"]}
onSelect={(option) => {
const newModel = [...triggeredModel];
newModel[0] = option;
setTriggeredModel(newModel);
}}
/>
<LabledDropdown
label="Triggered Point"
defaultOption={triggeredPoint[0] || "Select Point"}
options={["Point 1", "Point 2", "Point 3"]}
onSelect={(option) => {
const newPoint = [...triggeredPoint];
newPoint[0] = option;
setTriggeredPoint(newPoint);
}}
/>
<LabledDropdown
label="Triggered Action"
defaultOption={triggeredAction[0] || "Select Action"}
options={["Action 1", "Action 2", "Action 3"]}
onSelect={(option) => {
const newAction = [...triggeredAction];
newAction[0] = option;
setTriggeredAction(newAction);
}}
/>
</div>
</div>
</div>
</div>
);
);
};
export default Trigger;

View File

@ -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 ? (
<>
<div className="tools-container">
<div className="drop-down-icons">
<div className="activeDropicon">
{activeSubTool == "cursor" && (
<div
className={`tool-button ${
activeTool === "cursor" ? "active" : ""
}`}
onClick={() => {
setActiveTool("cursor");
}}
>
<CursorIcon isActive={activeTool === "cursor"} />
</div>
)}
{activeSubTool == "free-hand" && (
<div
className={`tool-button ${
activeTool === "free-hand" ? "active" : ""
}`}
onClick={() => {
setActiveTool("free-hand");
}}
>
<FreeMoveIcon isActive={activeTool === "free-hand"} />
</div>
)}
{activeSubTool == "delete" && (
<div
className={`tool-button ${
activeTool === "delete" ? "active" : ""
}`}
onClick={() => {
setActiveTool("delete");
}}
>
<DeleteIcon isActive={activeTool === "delete"} />
</div>
)}
{activeModule !== "visualization" && (
<div
className="drop-down-option-button"
ref={dropdownRef}
onClick={() => {
setOpenDrop(!openDrop);
}}
>
<ArrowIcon />
{openDrop && (
<div className="drop-down-container">
<div
className="option-list"
onClick={() => {
setOpenDrop(false);
setActiveTool("cursor");
setActiveSubTool("cursor");
}}
>
<div className="active-option">
{activeSubTool === "cursor" && <TickIcon />}
</div>
<CursorIcon isActive={false} />
<div className="option">Cursor</div>
</div>
<div
className="option-list"
onClick={() => {
setOpenDrop(false);
setActiveTool("free-hand");
setActiveSubTool("free-hand");
}}
>
<div className="active-option">
{activeSubTool === "free-hand" && <TickIcon />}
</div>
<FreeMoveIcon isActive={false} />
<div className="option">Free Hand</div>
</div>
<div
className="option-list"
onClick={() => {
setOpenDrop(false);
setActiveTool("delete");
setActiveSubTool("delete");
}}
>
<div className="active-option">
{activeSubTool === "delete" && <TickIcon />}
</div>
<DeleteIcon isActive={false} />
<div className="option">Delete</div>
</div>
</div>
)}
</div>
)}
</div>
</div>
{!toggleThreeD && activeModule === "builder" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`tool-button ${
activeTool === "draw-wall" ? "active" : ""
}`}
onClick={() => {
setActiveTool("draw-wall");
}}
title="Wall"
>
<WallIcon isActive={activeTool === "draw-wall"} />
</div>
<div
className={`tool-button ${
activeTool === "draw-zone" ? "active" : ""
}`}
onClick={() => {
setActiveTool("draw-zone");
}}
title="Zone"
>
<ZoneIcon isActive={activeTool === "draw-zone"} />
</div>
<div
className={`tool-button ${
activeTool === "draw-aisle" ? "active" : ""
}`}
onClick={() => {
setActiveTool("draw-aisle");
}}
title="Aisle"
>
<AsileIcon isActive={activeTool === "draw-aisle"} />
</div>
<div
className={`tool-button ${
activeTool === "draw-floor" ? "active" : ""
}`}
onClick={() => {
setActiveTool("draw-floor");
}}
title="Floor"
>
<FloorIcon isActive={activeTool === "draw-floor"} />
</div>
</div>
</>
)}
{activeModule === "builder" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`tool-button ${
activeTool === "measure" ? "active" : ""
}`}
onClick={() => {
setActiveTool("measure");
}}
title="Measure"
>
<MeasureToolIcon isActive={activeTool === "measure"} />
</div>
</div>
</>
)}
{activeModule === "simulation" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`tool-button ${
activeTool === "pen" ? "active" : ""
}`}
onClick={() => {
setActiveTool("pen");
}}
>
<PenIcon isActive={activeTool === "pen"} />
</div>
</div>
</>
)}
{activeModule === "visualization" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`tool-button`}
onClick={() => {
handleSaveTemplate({
addTemplate,
floatingWidget,
widgets3D,
selectedZone,
templates,
visualizationSocket,
});
}}
>
<SaveTemplateIcon isActive={false} />
</div>
</div>
</>
)}
<div className="split"></div>
<div className="general-options">
<div
className={`tool-button ${
activeTool === "comment" ? "active" : ""
}`}
onClick={() => {
setActiveTool("comment");
}}
>
<CommentIcon isActive={activeTool === "comment"} />
</div>
{toggleThreeD && (
<div className="tools-container">
<div className="drop-down-icons">
<div className="activeDropicon">
{activeSubTool == "cursor" && (
<div
className={`tool-button ${
activeTool === "play" ? "active" : ""
activeTool === "cursor" ? "active" : ""
}`}
onClick={() => {
setIsPlaying(!isPlaying);
setActiveTool("cursor");
}}
>
<PlayIcon isActive={activeTool === "play"} />
<CursorIcon isActive={activeTool === "cursor"} />
</div>
)}
{activeSubTool == "free-hand" && (
<div
className={`tool-button ${
activeTool === "free-hand" ? "active" : ""
}`}
onClick={() => {
setActiveTool("free-hand");
}}
>
<FreeMoveIcon isActive={activeTool === "free-hand"} />
</div>
)}
{activeSubTool == "delete" && (
<div
className={`tool-button ${
activeTool === "delete" ? "active" : ""
}`}
onClick={() => {
setActiveTool("delete");
}}
>
<DeleteIcon isActive={activeTool === "delete"} />
</div>
)}
{activeModule !== "visualization" && (
<div
className="drop-down-option-button"
ref={dropdownRef}
onClick={() => {
setOpenDrop(!openDrop);
}}
>
<ArrowIcon />
{openDrop && (
<div className="drop-down-container">
<div
className="option-list"
onClick={() => {
setOpenDrop(false);
setActiveTool("cursor");
setActiveSubTool("cursor");
}}
>
<div className="active-option">
{activeSubTool === "cursor" && <TickIcon />}
</div>
<CursorIcon isActive={false} />
<div className="option">Cursor</div>
</div>
<div
className="option-list"
onClick={() => {
setOpenDrop(false);
setActiveTool("free-hand");
setActiveSubTool("free-hand");
}}
>
<div className="active-option">
{activeSubTool === "free-hand" && <TickIcon />}
</div>
<FreeMoveIcon isActive={false} />
<div className="option">Free Hand</div>
</div>
<div
className="option-list"
onClick={() => {
setOpenDrop(false);
setActiveTool("delete");
setActiveSubTool("delete");
}}
>
<div className="active-option">
{activeSubTool === "delete" && <TickIcon />}
</div>
<DeleteIcon isActive={false} />
<div className="option">Delete</div>
</div>
</div>
)}
</div>
)}
</div>
{activeModule === "builder" && (
<>
<div className="split"></div>
</div>
{!toggleThreeD && activeModule === "builder" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`toggle-threed-button${
toggleThreeD ? " toggled" : ""
className={`tool-button ${
activeTool === "draw-wall" ? "active" : ""
}`}
onClick={toggleSwitch}
onClick={() => {
setActiveTool("draw-wall");
}}
title="Wall"
>
<div
className={`toggle-option${!toggleThreeD ? " active" : ""}`}
>
2d
</div>
<div
className={`toggle-option${toggleThreeD ? " active" : ""}`}
>
3d
</div>
<WallIcon isActive={activeTool === "draw-wall"} />
</div>
</>
<div
className={`tool-button ${
activeTool === "draw-zone" ? "active" : ""
}`}
onClick={() => {
setActiveTool("draw-zone");
}}
title="Zone"
>
<ZoneIcon isActive={activeTool === "draw-zone"} />
</div>
<div
className={`tool-button ${
activeTool === "draw-aisle" ? "active" : ""
}`}
onClick={() => {
setActiveTool("draw-aisle");
}}
title="Aisle"
>
<AsileIcon isActive={activeTool === "draw-aisle"} />
</div>
<div
className={`tool-button ${
activeTool === "draw-floor" ? "active" : ""
}`}
onClick={() => {
setActiveTool("draw-floor");
}}
title="Floor"
>
<FloorIcon isActive={activeTool === "draw-floor"} />
</div>
</div>
</>
)}
{activeModule === "builder" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`tool-button ${
activeTool === "measure" ? "active" : ""
}`}
onClick={() => {
setActiveTool("measure");
}}
title="Measure"
>
<MeasureToolIcon isActive={activeTool === "measure"} />
</div>
</div>
</>
)}
{activeModule === "simulation" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`tool-button ${
activeTool === "pen" ? "active" : ""
}`}
onClick={() => {
setActiveTool("pen");
}}
>
<PenIcon isActive={activeTool === "pen"} />
</div>
</div>
</>
)}
{activeModule === "visualization" && (
<>
<div className="split"></div>
<div className="draw-tools">
<div
className={`tool-button`}
onClick={() => {
handleSaveTemplate({
addTemplate,
floatingWidget,
widgets3D,
selectedZone,
templates,
visualizationSocket,
});
}}
>
<SaveTemplateIcon isActive={false} />
</div>
</div>
</>
)}
<div className="split"></div>
<div className="general-options">
<div
className={`tool-button ${
activeTool === "comment" ? "active" : ""
}`}
onClick={() => {
setActiveTool("comment");
}}
>
<CommentIcon isActive={activeTool === "comment"} />
</div>
{toggleThreeD && (
<div
className={`tool-button ${
activeTool === "play" ? "active" : ""
}`}
onClick={() => {
setIsPlaying(!isPlaying);
}}
>
<PlayIcon isActive={activeTool === "play"} />
</div>
)}
</div>
</>
{activeModule === "builder" && (
<>
<div className="split"></div>
<div
className={`toggle-threed-button${
toggleThreeD ? " toggled" : ""
}`}
onClick={toggleSwitch}
>
<div
className={`toggle-option${!toggleThreeD ? " active" : ""}`}
>
2d
</div>
<div
className={`toggle-option${toggleThreeD ? " active" : ""}`}
>
3d
</div>
</div>
</>
)}
</div>
) : (
<>
{activeModule !== "simulation" && (
<div className="exitPlay" onClick={() => setIsPlaying(false)}>
<button className="exitPlay" onClick={() => setIsPlaying(false)}>
X
</div>
</button>
)}
</>
)}

View File

@ -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 = ({
</div>
<span className={`expand-icon ${isTableOpen ? "open" : ""}`}>
{isTableOpen ? "⌵" : "⌵"}
<ArrowIcon />
</span>
</div>
<div

View File

@ -0,0 +1,95 @@
// LogList.tsx
import React, { useState } from "react";
import {
LogListIcon,
LogInfoIcon,
WarningIcon,
ErrorIcon,
CloseIcon,
} from "../../icons/ExportCommonIcons"; // Adjust path as needed
import { useLogger } from "./LoggerContext";
const LogList: React.FC = () => {
const { logs, clear, setIsLogListVisible } = useLogger();
const [selectedTab, setSelectedTab] = useState<
"all" | "info" | "warning" | "error" | "log"
>("all");
const getLogIcon = (type: string) => {
switch (type) {
case "info":
return <LogInfoIcon />;
case "error":
return <ErrorIcon />;
case "warning":
return <WarningIcon />;
case "log":
default:
return <LogInfoIcon />;
}
};
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
<div
className="log-list-container"
onClick={() => setIsLogListVisible(false)}
>
<div
className="log-list-wrapper"
onClick={(e) => {
e.stopPropagation();
}}
>
<div className="log-header">
<div className="log-header-wrapper">
<div className="icon">
<LogListIcon />
</div>
<div className="head">Log List</div>
</div>
<button className="close" onClick={() => setIsLogListVisible(false)}>
<CloseIcon />
</button>
</div>
{/* Tabs */}
<div className="log-nav-wrapper">
{["all", "info", "warning", "error"].map((type) => (
<div
key={type}
className={`log-nav ${selectedTab === type ? "active" : ""}`}
onClick={() => setSelectedTab(type as any)}
>
{`${type.charAt(0).toUpperCase() + type.slice(1)} Logs`}
</div>
))}
</div>
{/* Log Entries */}
<div className="log-entry-wrapper">
{filteredLogs.map((log) => (
<div key={log.id} className={`log-entry ${log.type}`}>
<div className="log-icon">{getLogIcon(log.type)}</div>
<div className="log-entry-message-container">
<div className="log-entry-message">{log.message}</div>
<div className="message-time">
{formatTimestamp(log.timestamp)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default LogList;

View File

@ -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<React.SetStateAction<LogEntry[]>>;
isLogListVisible: boolean;
setIsLogListVisible: React.Dispatch<React.SetStateAction<boolean>>;
log: (message: string) => void;
info: (message: string) => void;
warn: (message: string) => void;
error: (message: string) => void;
clear: () => void;
}
const LoggerContext = createContext<LoggerContextValue | undefined>(undefined);
export const LoggerProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [isLogListVisible, setIsLogListVisible] = useState<boolean>(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 (
<LoggerContext.Provider value={loggerMethods}>
{children}
</LoggerContext.Provider>
);
};
export const useLogger = () => {
const context = useContext(LoggerContext);
if (!context) {
throw new Error("useLogger must be used within a LoggerProvider");
}
return context;
};

View File

@ -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();

View File

@ -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<HTMLDivElement>(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<HTMLDivElement>(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<string[]>([]);
const [_, setProcessColors] = useState<string[]>([]);
// Generate colors on mount or when process changes
useEffect(() => {
@ -159,210 +165,236 @@ const SimulationPlayer: React.FC = () => {
};
return (
<div className="simulation-player-wrapper">
<div className={`simulation-player-container ${expand ? "open" : ""}`}>
<div className="controls-container">
<div className="production-details">
{/* hourlySimulation */}
<div className="hourly-wrapper production-wrapper">
<div className="header">
<div className="icon">
<HourlySimulationIcon />
<>
<div className="simulation-player-wrapper">
<div className={`simulation-player-container ${expand ? "open" : ""}`}>
<div className="controls-container">
{subModule === "analysis" && (
<div className="production-details">
{/* hourlySimulation */}
<div className="hourly-wrapper production-wrapper">
<div className="header">
<div className="icon">
<HourlySimulationIcon />
</div>
<div className="label">Hourly Simulation</div>
</div>
<div className="progress-wrapper">
<div
className="progress"
style={{ width: hourlySimulation }}
></div>
</div>
</div>
<div className="label">Hourly Simulation</div>
</div>
<div className="progress-wrapper">
<div
className="progress"
style={{ width: hourlySimulation }}
></div>
</div>
</div>
{/* dailyProduction */}
<div className="dailyProduction-wrapper production-wrapper">
<div className="header">
<div className="icon">
<DailyProductionIcon />
{/* dailyProduction */}
<div className="dailyProduction-wrapper production-wrapper">
<div className="header">
<div className="icon">
<DailyProductionIcon />
</div>
<div className="label">Daily Production</div>
</div>
<div className="progress-wrapper">
<div
className="progress"
style={{ width: dailyProduction }}
></div>
</div>
</div>
<div className="label">Daily Production</div>
</div>
<div className="progress-wrapper">
<div
className="progress"
style={{ width: dailyProduction }}
></div>
</div>
</div>
{/* monthlyROI */}
<div className="monthlyROI-wrapper production-wrapper">
<div className="header">
<div className="icon">
<MonthlyROI />
{/* monthlyROI */}
<div className="monthlyROI-wrapper production-wrapper">
<div className="header">
<div className="icon">
<MonthlyROI />
</div>
<div className="label">Monthly ROI</div>
</div>
<div className="progress-wrapper">
<div
className="progress"
style={{ width: monthlyROI }}
></div>
</div>{" "}
</div>
<div className="label">Monthly ROI</div>
</div>
<div className="progress-wrapper">
<div className="progress" style={{ width: monthlyROI }}></div>
</div>{" "}
</div>
</div>
<div className="controls-wrapper">
<button
className="simulation-button-container"
onClick={() => {
handleReset();
}}
>
<ResetIcon />
Reset
</button>
<button
className="simulation-button-container"
onClick={() => {
handlePlayStop();
}}
>
<PlayStopIcon />
{playSimulation ? "Play" : "Stop"}
</button>
<button
className="simulation-button-container"
onClick={() => {
handleExit();
}}
>
<ExitIcon />
Exit
</button>
<button
className="expand-icon-container"
onClick={() => setExpand(!expand)}
>
<ExpandIcon isActive={!expand} />
</button>
</div>
</div>
<div className="progresser-wrapper">
<div className="time-displayer">
<div className="start-time-wrappper">
<div className="icon">
<StartIcon />
)}
{subModule === "simulations" && (
<div className="header">
<InfoIcon />
{playSimulation
? "Paused - system idle."
: "Running simulation..."}
</div>
<div className="time-wrapper">
<div className="date">23 April ,25</div>
<div className="time">04:41 PM</div>
</div>
</div>
<div className="time-progresser">
<div className="timeline">
{intervals.map((label, index) => {
const segmentProgress = (index / totalSegments) * 100;
const isFilled = progress >= segmentProgress;
return (
<React.Fragment key={index}>
<div className="label-dot-wrapper">
<div className="label">{label} mins</div>
<div
className={`dot ${isFilled ? "filled" : ""}`}
></div>
</div>
{index < intervals.length - 1 && (
<div
className={`line ${
progress >= ((index + 1) / totalSegments) * 100
? "filled"
: ""
}`}
></div>
)}
</React.Fragment>
);
})}
</div>
</div>
<div className="end-time-wrappper">
<div className="time-wrapper">
<div className="time">00:10:20</div>
</div>
<div className="icon">
<EndIcon />
</div>
</div>
</div>
<div className="speed-control-container">
<div className="min-value">
<div className="icon">
<SpeedIcon />
</div>
Speed
</div>
<div className="slider-container" ref={sliderRef}>
<div className="speed-label mix-value">0.5X</div>
<div className="marker marker-10"></div>
<div className="marker marker-20"></div>
<div className="marker marker-30"></div>
<div className="marker marker-40"></div>
<div className="marker marker-50"></div>
<div className="marker marker-60"></div>
<div className="marker marker-70"></div>
<div className="marker marker-80"></div>
<div className="marker marker-90"></div>
<div className="custom-slider">
)}
<div className="controls-wrapper">
<button
className="simulation-button-container"
onClick={() => {
handleReset();
}}
>
<ResetIcon />
Reset
</button>
<button
className="simulation-button-container"
onClick={() => {
handlePlayStop();
}}
>
<PlayStopIcon />
{playSimulation ? "Play" : "Stop"}
</button>
<button
className="simulation-button-container"
onClick={() => {
handleExit();
}}
>
<ExitIcon />
Exit
</button>
{subModule === "analysis" && (
<button
className={`slider-handle ${isDragging ? "dragging" : ""}`}
style={{ left: `${calculateHandlePosition()}%` }}
onMouseDown={handleMouseDown}
className="expand-icon-container"
onClick={() => setExpand(!expand)}
>
{speed.toFixed(1)}x
<ExpandIcon isActive={!expand} />
</button>
<input
type="range"
min="0.5"
max={MAX_SPEED}
step="0.1"
value={speed}
onChange={handleSpeedChange}
className="slider-input"
/>
</div>
<div className="speed-label max-value">4x</div>
)}
</div>
</div>
</div>
<div className="processDisplayer">
<div className="start-displayer timmer">00:00</div>
<div className="end-displayer timmer">24:00</div>
<div
className="process-wrapper"
style={{ padding: expand ? "0px" : "5px 35px" }}
>
<div
className="process-container"
ref={processWrapperRef}
onMouseDown={handleProcessMouseDown}
>
{process.map((item, index) => (
<div
key={index}
className="process"
style={{
width: `${item.completed}%`,
backgroundColor: getAvatarColor(index),
}}
>
<div
className="process-player"
ref={processPlayerRef}
style={{ left: playerPosition, position: "absolute" }}
></div>
<div className="progresser-wrapper">
<div className="time-displayer">
<div className="start-time-wrappper">
<div className="icon">
<StartIcon />
</div>
))}
<div className="time-wrapper">
<div className="date">23 April ,25</div>
<div className="time">04:41 PM</div>
</div>
</div>
<div className="time-progresser">
<div className="timeline">
{intervals.map((label, index) => {
const segmentProgress = (index / totalSegments) * 100;
const isFilled = progress >= segmentProgress;
return (
<React.Fragment key={index}>
<div className="label-dot-wrapper">
<div className="label">{label} mins</div>
<div
className={`dot ${isFilled ? "filled" : ""}`}
></div>
</div>
{index < intervals.length - 1 && (
<div
className={`line ${
progress >= ((index + 1) / totalSegments) * 100
? "filled"
: ""
}`}
></div>
)}
</React.Fragment>
);
})}
</div>
</div>
<div className="end-time-wrappper">
<div className="time-wrapper">
<div className="time">00:10:20</div>
</div>
<div className="icon">
<EndIcon />
</div>
</div>
</div>
<div className="speed-control-container">
<div className="min-value">
<div className="icon">
<SpeedIcon />
</div>
Speed
</div>
<div className="slider-container" ref={sliderRef}>
<div className="speed-label mix-value">0.5X</div>
<div className="marker marker-10"></div>
<div className="marker marker-20"></div>
<div className="marker marker-30"></div>
<div className="marker marker-40"></div>
<div className="marker marker-50"></div>
<div className="marker marker-60"></div>
<div className="marker marker-70"></div>
<div className="marker marker-80"></div>
<div className="marker marker-90"></div>
<div className="custom-slider">
<button
className={`slider-handle ${isDragging ? "dragging" : ""}`}
style={{ left: `${calculateHandlePosition()}%` }}
onMouseDown={handleMouseDown}
>
{speed.toFixed(1)}x
</button>
<input
type="range"
min="0.5"
max={MAX_SPEED}
step="0.1"
value={speed}
onChange={handleSpeedChange}
className="slider-input"
/>
</div>
<div className="speed-label max-value">4x</div>
</div>
</div>
</div>
{subModule === "analysis" && (
<div className="processDisplayer">
<div className="start-displayer timmer">00:00</div>
<div className="end-displayer timmer">24:00</div>
<div
className="process-wrapper"
style={{ padding: expand ? "0px" : "5px 35px" }}
>
<div
className="process-container"
ref={processWrapperRef}
onMouseDown={handleProcessMouseDown}
>
{process.map((item, index) => (
<div
key={index}
className="process"
style={{
width: `${item.completed}%`,
backgroundColor: getAvatarColor(index),
}}
>
<div
className="process-player"
ref={processPlayerRef}
style={{ left: playerPosition, position: "absolute" }}
></div>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
<div className="analysis">
<div className="analysis-wrapper">
<ProductionCapacity />
<ThroughputSummary />
</div>
<ROISummary />
</div>
</>
);
};

View File

@ -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: []
}

View File

@ -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: []

View File

@ -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);

View File

@ -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<Types.FloorItems>([]);
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;

View File

@ -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<Types.FloorItems>([]);
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<THREE.Vector2 | null>(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(

View File

@ -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<any>(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 (
<group key={i} position={[...event.position]} rotation={[...event.rotation]} >
<group
key={i}
position={new THREE.Vector3(...event.position)}
>
{event.points.map((point, j) => (
<mesh
name="Event-Sphere"
@ -94,14 +152,16 @@ function PointsCreator() {
}}
onPointerMissed={() => {
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,
}}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="orange" />
@ -111,7 +171,10 @@ function PointsCreator() {
);
} else if (event.type === "vehicle") {
return (
<group key={i} position={[...event.position]} rotation={[...event.rotation]} >
<group
key={i}
position={new THREE.Vector3(...event.position)}
>
<mesh
name="Event-Sphere"
uuid={event.point.uuid}
@ -122,13 +185,11 @@ function PointsCreator() {
sphereRefs.current[event.point.uuid]
);
}}
onPointerMissed={() => {
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,
}}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="blue" />
@ -137,7 +198,10 @@ function PointsCreator() {
);
} else if (event.type === "roboticArm") {
return (
<group key={i} position={[...event.position]} rotation={[...event.rotation]} >
<group
key={i}
position={new THREE.Vector3(...event.position)}
>
<mesh
name="Event-Sphere"
uuid={event.point.uuid}
@ -149,12 +213,13 @@ function PointsCreator() {
);
}}
onPointerMissed={() => {
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,
}}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="green" />
@ -163,7 +228,10 @@ function PointsCreator() {
);
} else if (event.type === "machine") {
return (
<group key={i} position={[...event.position]} rotation={[...event.rotation]} >
<group
key={i}
position={new THREE.Vector3(...event.position)}
>
<mesh
name="Event-Sphere"
uuid={event.point.uuid}
@ -175,12 +243,14 @@ function PointsCreator() {
);
}}
onPointerMissed={() => {
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,
}}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="purple" />
@ -208,4 +278,4 @@ function PointsCreator() {
);
}
export default PointsCreator;
export default PointsCreator;

View File

@ -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<boolean>(false);
const isPausedRef = useRef<boolean>(false);
const startTimeRef = useRef<number>(0);
const animationFrameId = useRef<number | null>(null);
const pauseTimeRef = useRef<number | null>(null);
const { isPaused } = usePauseButtonStore();
const { removeCurrentAction } = useMachineStore();
const { isReset, setReset } = useResetButtonStore();
const { isPlaying } = usePlayButtonStore();
const { speed } = useAnimationPlaySpeed();
const isPlayingRef = useRef<boolean>(false);
const isResetRef = useRef<boolean>(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;

View File

@ -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<string>('idle');
let isIncrememtable = useRef<boolean>(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 (
<>
<MachineAnimator processingTime={machineDetail.point.action.processTime} handleCallBack={handleCallBack} currentPhase={currentPhase} machineUuid={machineDetail.modelUuid} machineStatus={machineStatus} reset={reset} />
</>
)
}

View File

@ -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 (
<>
<MachineInstance />
{machines.map((val: MachineStatus) => (
<MachineInstance key={val.modelUuid} machineDetail={val} />
))}
</>
)

View File

@ -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 (
<>

View File

@ -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 (
<></>
)

View File

@ -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 (
<>

View File

@ -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<THREE.CatmullRomCurve3 | null>(null);
const initialStartPositionRef = useRef<THREE.Vector3 | null>(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<THREE.Vector3[] | null>(null);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const [circlePoints, setCirclePoints] = useState<[number, number, number][]>([]);
const [customCurvePoints, setCustomCurvePoints] = useState<THREE.Vector3[] | null>(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 && (
<mesh rotation={armBot.rotation} position={armBot.position}>
<Line
points={customCurvePoints.map((p) => [p.x, p.y, p.z] as [number, number, number])}
color="green"
lineWidth={5}
dashed={false}
/>
</mesh>
)}
<mesh position={[armBot.position[0], armBot.position[1] + 1.5, armBot.position[2]]} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[1.59, 1.61, 64]} />
<meshBasicMaterial color="green" side={THREE.DoubleSide} />
</mesh>
</>
);
}
export default RoboticArmAnimator;
export default RoboticArmAnimator;

View File

@ -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<any>(null);
const { addCurrentAction, setArmBotActive, setArmBotState, removeCurrentAction } = useArmBotStore();
const { floorItems } = useFloorItems();
const groupRef = useRef<any>(null);
const [processes, setProcesses] = useState<Process[]>([]);
const [armBotCurvePoints, setArmBotCurvePoints] = useState({ start: [], end: [] })
const restPosition = new THREE.Vector3(0, 1, -1.6);
let armBotCurveRef = useRef<THREE.CatmullRomCurve3 | null>(null)
const [path, setPath] = useState<[number, number, number][]>([]);
const [ikSolver, setIkSolver] = useState<any>(null);
const { scene } = useThree();
const restPosition = new THREE.Vector3(0, 1.75, -1.6);
const targetBone = "Target";
const groupRef = useRef<any>(null);
const pauseTimeRef = useRef<number | null>(null);
const isPausedRef = useRef<boolean>(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 (
<>
<IKInstance modelUrl={armModel} setIkSolver={setIkSolver} ikSolver={ikSolver} robot={robot} groupRef={groupRef} processes={processes}
setArmBotCurvePoints={setArmBotCurvePoints} />
<RoboticArmAnimator armUuid={robot?.modelUuid} HandleCallback={HandleCallback}
currentPhase={currentPhase} targetBone={targetBone} ikSolver={ikSolver} robot={robot}
logStatus={logStatus} groupRef={groupRef} processes={processes} armBotCurveRef={armBotCurveRef} path={path} />
<IKInstance modelUrl={armModel} setIkSolver={setIkSolver} ikSolver={ikSolver} armBot={armBot} groupRef={groupRef} />
<RoboticArmAnimator HandleCallback={HandleCallback} restPosition={restPosition} ikSolver={ikSolver} targetBone={targetBone} armBot={armBot}
logStatus={logStatus} path={path} currentPhase={currentPhase} />
</>
)
}
export default RoboticArmInstance;
export default RoboticArmInstance;

View File

@ -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<THREE.Group>;
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<THREE.Group>();
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 (
<>
<group ref={groupRef} position={robot.position} rotation={robot.rotation}>
<group ref={groupRef} position={armBot.position} rotation={armBot.rotation} onClick={() => {
setSelectedArm(groupRef.current?.getObjectByName(targetBoneName))
}}>
<primitive
uuid={"ArmBot-X200"}
object={cloned}
@ -82,6 +87,7 @@ function IKInstance({ modelUrl, setIkSolver, ikSolver, robot, groupRef, processe
name={`arm-bot11`}
/>
</group>
{/* {selectedArm && <TransformControls object={selectedArm} />} */}
</>
)
}

View File

@ -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) => (
<RoboticArmInstance key={robot.modelUuid} robot={robot} />
<RoboticArmInstance key={robot.modelUuid} armBot={robot} />
))}
</>
)
}

View File

@ -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 (
<>
<RoboticArmInstances />
{selectedEventSphere && selectedEventData?.data.type === "roboticArm" &&
< ArmBotUI />
}
</>
);
}

View File

@ -23,20 +23,20 @@ function Simulation() {
}, [events])
useEffect(() => {
// console.log('products: ', products);
console.log('products: ', products);
}, [products])
return (
<>
<Products />
{activeModule === 'simulation' &&
<>
<Points />
<Products />
<Materials />
<Trigger />

View File

@ -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<string | null>(null);
const groupRefs = useRef<Record<string, any>>({});
const [helperlineColor, setHelperLineColor] = useState<string>("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<ConnectionLine[]>([]);
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 (
<>
</>
<group name="simulationConnectionGroup" >
{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 (
<QuadraticBezierLine
key={connection.id}
ref={(el) => (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 && (
<QuadraticBezierLine
start={currentLine.start.toArray()}
end={currentLine.end.toArray()}
mid={currentLine.mid.toArray()}
color={helperlineColor}
lineWidth={4}
dashed
dashSize={1}
dashScale={20}
/>
)}
</group>
);
}

View File

@ -9,7 +9,7 @@ interface PickDropProps {
actionType: "pick" | "drop";
actionUuid: string;
gltfScene: THREE.Group;
selectedPoint: THREE.Mesh | null;
handlePointerDown: (e: ThreeEvent<PointerEvent>) => void;
isSelected: boolean;
}
@ -21,12 +21,10 @@ const PickDropPoints: React.FC<PickDropProps> = ({
actionType,
actionUuid,
gltfScene,
selectedPoint,
handlePointerDown,
isSelected,
}) => {
const groupRef = useRef<THREE.Group>(null);
return (
<group
ref={groupRef}
@ -36,15 +34,24 @@ const PickDropPoints: React.FC<PickDropProps> = ({
: 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 }}
>
<primitive
object={gltfScene.clone()}
position={[0, 0, 0]} // Ensure this stays at origin
object={(() => {
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]}
/>
</group>

View File

@ -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<any>(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 (
<React.Fragment key={action.actionUuid}>
<group
position={new THREE.Vector3(...selectedArmBotData.position)}
rotation={new THREE.Euler(...selectedArmBotData.rotation)}
>
<PickDropPoints
position={startPosition}
modelUuid={selectedArmBotData.modelUuid}
pointUuid={selectedArmBotData.point.uuid}
actionType="pick"
actionUuid={action.actionUuid}
gltfScene={armUiPick.scene}
handlePointerDown={handlePointerDown}
isSelected={true}
/>
<PickDropPoints
position={endPosition}
modelUuid={selectedArmBotData.modelUuid}
pointUuid={selectedArmBotData.point.uuid}
actionType="drop"
actionUuid={action.actionUuid}
gltfScene={armUiDrop.scene}
handlePointerDown={handlePointerDown}
isSelected={true}
/>
</group>
</React.Fragment>
);
} else {
return null; // important! must return something
}
})}
</>
);
};
export default ArmBotUI;

View File

@ -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<THREE.Object3D | null>(null);
const planeRef = useRef<THREE.Plane>(
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<PointerEvent>) => {
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 };
}

View File

@ -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<THREE.Group>(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");

View File

@ -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<boolean>(false);
const isPausedRef = useRef<boolean>(false);
const pauseTimeRef = useRef<number | null>(null);
const [progress, setProgress] = useState<number>(0);
const [restRotation, setRestingRotation] = useState<boolean>(true);
const [currentPath, setCurrentPath] = useState<[number, number, number][]>([]);
const [progress, setProgress] = useState<number>(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);

View File

@ -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,

View File

@ -9,10 +9,8 @@ function VehicleInstances() {
return (
<>
{vehicles.map((val: any, i: any) =>
<VehicleInstance agvDetail={val} key={i} />
{vehicles.map((val: VehicleStatus) =>
<VehicleInstance agvDetail={val} key={val.modelUuid} />
)}
</>

View File

@ -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 (
<>
<VehicleInstances />
{selectedEventSphere && selectedEventData?.data.type === "vehicle" && !isPlaying &&
< VehicleUI />
}
</>
);
}
export default Vehicles;
export default Vehicles;

View File

@ -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<HTMLDivElement>) => {
event.preventDefault();
try {
const email = localStorage.getItem("email") ?? "";
const organization = email?.split("@")[1]?.split(".")[0];
// const handleDrop = async (event: React.DragEvent<HTMLDivElement>) => {
// 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 = () => {
}
`}
</style>
<div
ref={containerRef}
id="real-time-vis-canvas"
className={`realTime-viz canvas ${isPlaying ? "playingFlase" : ""}`}
style={{
height: isPlaying || activeModule !== "visualization" ? "100vh" : "",
width: isPlaying || activeModule !== "visualization" ? "100vw" : "",
left: isPlaying || activeModule !== "visualization" ? "0%" : "",
}}
>
<div ref={containerRef} className="realTime-viz">
<div className="realTime-viz-wrapper">
{openConfirmationPopup && (
<RenderOverlay>
@ -324,20 +312,6 @@ const RealTimeVisulization: React.FC = () => {
/>
</RenderOverlay>
)}
<div
className="scene-container"
style={{
height: "100%",
width: "100%",
borderRadius:
isPlaying || activeModule !== "visualization" ? "" : "6px",
}}
role="application"
onDrop={(event) => handleDrop(event)}
onDragOver={(event) => event.preventDefault()}
>
<Scene />
</div>
{activeModule === "visualization" && selectedZone.zoneName !== "" && (
<DroppedObjects />
)}

View File

@ -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<HTMLDivElement>
}
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);
}
};

View File

@ -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 (
<div className="project-main">
{/* <div className="analysis">
<div className="analysis-wrapper">
<ProductionCapacity />
<ThroughputSummary />
</div>
<ROISummary />
</div> */}
<KeyPressListener />
{loadingProgress > 0 && <LoadingPage progress={loadingProgress} />}
{!isPlaying && (
{!selectedUser && (
<>
{toggleThreeD && <ModuleToggle />}
<SideBarLeft />
<SideBarRight />
<KeyPressListener />
{loadingProgress > 0 && <LoadingPage progress={loadingProgress} />}
{!isPlaying && (
<>
{toggleThreeD && <ModuleToggle />}
<SideBarLeft />
<SideBarRight />
</>
)}
<RealTimeVisulization />
{activeModule === "market" && <MarketPlace />}
{activeModule !== "market" && <Tools />}
{isPlaying && activeModule === "simulation" && <SimulationPlayer />}
</>
)}
{/* <RenderOverlay>
<MenuBar setOpenMenu={setOpenMenu} />
</RenderOverlay> */}
{activeModule === "market" && <MarketPlace />}
<RealTimeVisulization />
{activeModule !== "market" && <Tools />}
{isPlaying && activeModule === "simulation" && <SimulationPlayer />}
{/* {<SimulationPlayer />} */}
<div
className="scene-container"
id="real-time-vis-canvas"
style={{
height: isPlaying || activeModule !== "visualization" ? "100vh" : "",
width: isPlaying || activeModule !== "visualization" ? "100vw" : "",
left: isPlaying || activeModule !== "visualization" ? "0%" : "",
borderRadius:
isPlaying || activeModule !== "visualization" ? "" : "6px",
}}
role="application"
onDrop={(event) =>
createHandleDrop({
widgetSubOption,
visualizationSocket,
selectedZone,
setFloatingWidget,
event,
})
}
onDragOver={(event) => event.preventDefault()}
>
<Scene />
</div>
{selectedUser && <FollowPerson />}
{isLogListVisible && (
<RenderOverlay>
<LogList />
</RenderOverlay>
)}
<Footer />
</div>
);
};

View File

@ -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");
}
}
};

View File

@ -10,6 +10,7 @@ interface ArmBotStore {
modelUuid: string,
updates: Partial<Omit<ArmBotStatus, 'modelUuid' | 'productId'>>
) => void;
clearArmBots: () => void;
addCurrentAction: (modelUuid: string, actionUuid: string) => void;
removeCurrentAction: (modelUuid: string) => void;
@ -39,14 +40,17 @@ export const useArmBotStore = create<ArmBotStore>()(
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<ArmBotStore>()(
});
},
clearArmBots: () => {
set((state) => {
state.armBots = [];
});
},
addCurrentAction: (modelUuid, actionUuid) => {
set((state) => {
const armBot = state.armBots.find(a => a.modelUuid === modelUuid);

View File

@ -10,6 +10,7 @@ interface ConveyorStore {
modelUuid: string,
updates: Partial<Omit<ConveyorStatus, 'modelUuid' | 'productId'>>
) => void;
clearConveyors: () => void;
setConveyorActive: (modelUuid: string, isActive: boolean) => void;
setConveyorState: (modelUuid: string, newState: ConveyorStatus['state']) => void;
@ -30,14 +31,17 @@ export const useConveyorStore = create<ConveyorStore>()(
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<ConveyorStore>()(
});
},
clearConveyors: () => {
set((state) => {
state.conveyors = [];
});
},
setConveyorActive: (modelUuid, isActive) => {
set((state) => {
const conveyor = state.conveyors.find(c => c.modelUuid === modelUuid);

View File

@ -7,7 +7,7 @@ type EventsStore = {
// Event-level actions
addEvent: (event: EventsSchema) => void;
removeEvent: (modelUuid: string) => void;
updateEvent: (modelUuid: string, updates: Partial<EventsSchema>) => void;
updateEvent: (modelUuid: string, updates: Partial<EventsSchema>) => EventsSchema | undefined;
// Point-level actions
addPoint: (modelUuid: string, point: ConveyorPointSchema | VehiclePointSchema | RoboticArmPointSchema | MachinePointSchema | StoragePointSchema) => void;
@ -49,7 +49,9 @@ export const useEventsStore = create<EventsStore>()(
// 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<EventsStore>()(
},
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<EventsStore>()(
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<EventsStore>()(
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<EventsStore>()(
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;
}

View File

@ -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<Omit<MachineStatus, 'modelUuid' | 'productId'>>
) => 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<MachineStore>()(
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<MachineStore>()(
});
},
// 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<MachineStore>()(
});
},
// Time tracking
incrementActiveTime: (modelUuid, incrementBy) => {
set((state) => {
const machine = state.machines.find(m => m.modelUuid === modelUuid);
@ -99,7 +129,6 @@ export const useMachineStore = create<MachineStore>()(
});
},
// Helpers
getMachineById: (modelUuid) => {
return get().machines.find(m => m.modelUuid === modelUuid);
},

View File

@ -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<ConveyorPointSchema['action'] | VehiclePointSchema['action'] | RoboticArmPointSchema['actions'][0] | MachinePointSchema['action'] | StoragePointSchema['action']>
) => 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<TriggerSchema>
) => 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<ProductsStore>()(
// 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<ProductsStore>()(
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<ProductsStore>()(
});
},
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<ProductsStore>()(
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<ProductsStore>()(
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<ProductsStore>()(
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<ProductsStore>()(
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<ProductsStore>()(
},
// 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<ProductsStore>()(
}
}
});
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<ProductsStore>()(
}
}
});
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<ProductsStore>()(
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<ProductsStore>()(
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<ProductsStore>()(
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<ProductsStore>()(
}
}
});
return updatedEvent;
},
// Renaming functions
@ -388,10 +446,11 @@ export const useProductStore = create<ProductsStore>()(
});
},
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<ProductsStore>()(
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) {

View File

@ -114,4 +114,36 @@ export const useSelectedAction = create<SelectedActionState>()(
});
},
}))
);
interface IsDraggingState {
isDragging: "start" | "end" | null;
setIsDragging: (state: "start" | "end" | null) => void;
}
export const useIsDragging = create<IsDraggingState>()(
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<IsRotatingState>()(
immer((set) => ({
isRotating: null,
setIsRotating: (state) => {
set((s) => {
s.isRotating = state;
});
},
}))
);

View File

@ -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<Omit<StorageUnitStatus, 'modelUuid' | 'productId'>>
) => 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<StorageUnitStore>()(
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<StorageUnitStore>()(
});
},
// 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<StorageUnitStore>()(
});
},
// Load updates
updateStorageUnitLoad: (modelUuid, incrementBy) => {
set((state) => {
const unit = state.storageUnits.find(s => s.modelUuid === modelUuid);
@ -96,7 +98,6 @@ export const useStorageUnitStore = create<StorageUnitStore>()(
});
},
// Time tracking
incrementActiveTime: (modelUuid, incrementBy) => {
set((state) => {
const unit = state.storageUnits.find(s => s.modelUuid === modelUuid);
@ -115,7 +116,6 @@ export const useStorageUnitStore = create<StorageUnitStore>()(
});
},
// Helpers
getStorageUnitById: (modelUuid) => {
return get().storageUnits.find(s => s.modelUuid === modelUuid);
},

View File

@ -20,6 +20,7 @@ interface VehiclesStore {
modelUuid: string,
updates: Partial<Omit<VehicleStatus, 'modelUuid' | 'productId'>>
) => void;
clearvehicles: () => void;
setVehicleActive: (modelUuid: string, isActive: boolean) => void;
updateSteeringAngle: (modelUuid: string, steeringAngle: number) => void;
@ -41,15 +42,18 @@ export const useVehicleStore = create<VehiclesStore>()(
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<VehiclesStore>()(
});
},
clearvehicles: () => {
set((state) => {
state.vehicles = [];
});
},
setVehicleActive: (modelUuid, isActive) => {
set((state) => {
const vehicle = state.vehicles.find(v => v.modelUuid === modelUuid);

View File

@ -1,5 +1,4 @@
import { create } from "zustand";
import { addingFloatingWidgets } from "../../services/visulization/zone/addFloatingWidgets";
import { useSocketStore } from "../store";
import useChartStore from "./useChartStore";

View File

@ -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;

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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%;
}
}
}
}
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}
}
}

View File

@ -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;

View File

@ -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);
}
}
}
}
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;

View File

@ -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%;

View File

@ -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';

View File

@ -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;

View File

@ -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 {

View File

@ -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
}
}