added arm ui

This commit is contained in:
Nalvazhuthi 2025-04-28 12:26:31 +05:30
parent c5d4679068
commit 3ebadf3c10
14 changed files with 1413 additions and 489 deletions

66
app/package-lock.json generated
View File

@ -30,7 +30,6 @@
"glob": "^11.0.0",
"gsap": "^3.12.5",
"html2canvas": "^1.4.1",
"immer": "^10.1.1",
"leva": "^0.10.0",
"mqtt": "^5.10.4",
"postprocessing": "^6.36.4",
@ -2022,7 +2021,7 @@
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"devOptional": true,
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
},
@ -2034,7 +2033,7 @@
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"devOptional": true,
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
@ -4137,26 +4136,6 @@
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz",
@ -4268,25 +4247,25 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"devOptional": true
"dev": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"devOptional": true
"dev": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"devOptional": true
"dev": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"devOptional": true
"dev": true
},
"node_modules/@turf/along": {
"version": "7.2.0",
@ -9040,7 +9019,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"devOptional": true
"dev": true
},
"node_modules/cross-env": {
"version": "7.0.3",
@ -9917,7 +9896,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"devOptional": true,
"dev": true,
"engines": {
"node": ">=0.3.1"
}
@ -12747,10 +12726,9 @@
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"license": "MIT",
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@ -15281,7 +15259,7 @@
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"devOptional": true
"dev": true
},
"node_modules/makeerror": {
"version": "1.0.12",
@ -18012,16 +17990,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/react-dev-utils/node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/react-dev-utils/node_modules/loader-utils": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz",
@ -20759,7 +20727,7 @@
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
"devOptional": true,
"dev": true,
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@ -20802,7 +20770,7 @@
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
"devOptional": true,
"dev": true,
"dependencies": {
"acorn": "^8.11.0"
},
@ -20814,7 +20782,7 @@
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"devOptional": true
"dev": true
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
@ -21310,7 +21278,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"devOptional": true
"dev": true
},
"node_modules/v8-to-istanbul": {
"version": "8.1.1",
@ -22369,7 +22337,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"devOptional": true,
"dev": true,
"engines": {
"node": ">=6"
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,93 @@
export function ThroughputSummaryIcon() {
return (
<svg
width="26"
height="27"
viewBox="0 0 26 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="13.3457" cy="13.498" r="12.6543" fill="#FC9D2F" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.9063 12.9046L14.2265 13.86L14.4378 14.5073C14.9906 16.2239 15.2594 17.2662 15.2594 17.7299C15.2594 18.8219 14.3742 19.7072 13.2822 19.7072C12.1902 19.7072 11.305 18.8219 11.305 17.7299C11.305 17.2106 11.6422 15.9654 12.3379 13.86L12.658 12.9046C12.8604 12.3082 13.704 12.3082 13.9063 12.9046ZM13.2822 7.84375C16.9222 7.84375 19.873 10.7945 19.873 14.4345C19.873 15.7565 19.4823 17.0219 18.7621 18.0974C18.5596 18.3999 18.1502 18.4809 17.8478 18.2784C17.5453 18.0758 17.4643 17.6665 17.6668 17.364C18.2428 16.5038 18.5548 15.4933 18.5548 14.4345C18.5548 11.5225 16.1942 9.16191 13.2822 9.16191C10.3702 9.16191 8.00956 11.5225 8.00956 14.4345C8.00956 15.4933 8.32153 16.5038 8.89752 17.364C9.10005 17.6665 9.01904 18.0758 8.71659 18.2784C8.41414 18.4809 8.00477 18.3999 7.80224 18.0974C7.08206 17.0219 6.69141 15.7565 6.69141 14.4345C6.69141 10.7945 9.6422 7.84375 13.2822 7.84375ZM13.2822 15.2247L13.0657 15.9238L12.9161 16.4319C12.7219 17.111 12.6231 17.5509 12.6231 17.7299C12.6231 18.0939 12.9182 18.389 13.2822 18.389C13.6462 18.389 13.9413 18.0939 13.9413 17.7299C13.9413 17.511 13.7936 16.9022 13.5044 15.9428L13.2822 15.2247Z"
fill="white"
/>
</svg>
);
}
export function ProductionCapacityIcon() {
return (
<svg
width="26"
height="27"
viewBox="0 0 26 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="13.3457" cy="13.498" r="12.6543" fill="#FC9D2F" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.9063 12.9046L14.2265 13.86L14.4378 14.5073C14.9906 16.2239 15.2594 17.2662 15.2594 17.7299C15.2594 18.8219 14.3742 19.7072 13.2822 19.7072C12.1902 19.7072 11.305 18.8219 11.305 17.7299C11.305 17.2106 11.6422 15.9654 12.3379 13.86L12.658 12.9046C12.8604 12.3082 13.704 12.3082 13.9063 12.9046ZM13.2822 7.84375C16.9222 7.84375 19.873 10.7945 19.873 14.4345C19.873 15.7565 19.4823 17.0219 18.7621 18.0974C18.5596 18.3999 18.1502 18.4809 17.8478 18.2784C17.5453 18.0758 17.4643 17.6665 17.6668 17.364C18.2428 16.5038 18.5548 15.4933 18.5548 14.4345C18.5548 11.5225 16.1942 9.16191 13.2822 9.16191C10.3702 9.16191 8.00956 11.5225 8.00956 14.4345C8.00956 15.4933 8.32153 16.5038 8.89752 17.364C9.10005 17.6665 9.01904 18.0758 8.71659 18.2784C8.41414 18.4809 8.00477 18.3999 7.80224 18.0974C7.08206 17.0219 6.69141 15.7565 6.69141 14.4345C6.69141 10.7945 9.6422 7.84375 13.2822 7.84375ZM13.2822 15.2247L13.0657 15.9238L12.9161 16.4319C12.7219 17.111 12.6231 17.5509 12.6231 17.7299C12.6231 18.0939 12.9182 18.389 13.2822 18.389C13.6462 18.389 13.9413 18.0939 13.9413 17.7299C13.9413 17.511 13.7936 16.9022 13.5044 15.9428L13.2822 15.2247Z"
fill="white"
/>
</svg>
);
}
export function ROISummaryIcon() {
return (
<svg
width="25"
height="26"
viewBox="0 0 25 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect y="0.515625" width="25" height="25" rx="12.5" fill="#28B9F3" />
<path
d="M6.00015 7.51562V19.0974H19.0002"
stroke="white"
stroke-linecap="round"
/>
<path
d="M6.50037 15.0553L10.3102 11.847C10.6984 11.52 11.2701 11.5358 11.6397 11.8837L15.0095 15.0553L19.5004 11.2734"
stroke="white"
stroke-linecap="round"
/>
</svg>
);
}
export function PowerIcon() {
return (
<svg
width="21"
height="21"
viewBox="0 0 21 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_4107_3144)">
<path
d="M12.1277 1.76564L10.7174 9.17535L15.8265 9.19254L8.87213 19.2375L10.2824 11.0856L5.17369 11.0678L12.1277 1.76564ZM12.1287 0.515664C12.0949 0.515664 12.0612 0.516895 12.0281 0.519375C11.8075 0.537207 11.6612 0.610957 11.4878 0.72752C11.3901 0.792624 11.3021 0.871096 11.2262 0.960645C11.2034 0.987526 11.1819 1.01547 11.1618 1.04439L4.15775 10.3141C3.88119 10.6931 3.84056 11.1935 4.05306 11.6116C4.26525 12.0297 4.69431 12.2947 5.16463 12.2982L8.77275 12.3244L7.63838 19.0079C7.53056 19.5822 7.83681 20.1547 8.37588 20.3854C8.53254 20.4527 8.70128 20.4873 8.87179 20.4872C9.26461 20.4872 9.58742 20.3035 9.82963 19.9716L16.8424 9.92658C17.119 9.5475 17.1593 9.04656 16.9471 8.62906C16.7349 8.21094 16.3059 7.94592 15.8356 7.9425L12.2274 7.93625L13.3496 2.05969C13.3734 1.96348 13.3854 1.86473 13.3853 1.76562C13.3853 1.08938 12.8468 0.538125 12.1731 0.51625C12.1581 0.515625 12.1434 0.515625 12.1287 0.515625L12.1287 0.515664Z"
fill="#F3C64D"
/>
<path
d="M4.875 11.125L12.375 1.125L11.125 8.625H16.125L8.625 19.875L9.875 11.125H4.875Z"
fill="#F3C64D"
/>
</g>
<defs>
<clipPath id="clip0_4107_3144">
<rect
width="20"
height="20"
fill="white"
transform="translate(0.5 0.5)"
/>
</clipPath>
</defs>
</svg>
);
}

View File

@ -0,0 +1,62 @@
import React from "react";
import { ProductionCapacityIcon } from "../../icons/analysis";
const ProductionCapacity = () => {
const totalBars = 6;
const progressPercent = 50;
const barsToFill = Math.floor((progressPercent / 100) * totalBars);
const partialFillPercent =
((progressPercent / 100) * totalBars - barsToFill) * 100;
return (
<div className="productionCapacity-container analysis-card">
<div className="productionCapacity-wrapper analysis-card-wrapper">
<div className="card-header">
<div className="header">
<div className="main-header">Throughput Summary</div>
<div className="sub-header">08:00 - 09:00 AM</div>
</div>
<div className="icon-wrapper">
<ProductionCapacityIcon />
</div>
</div>
<div className="process-container">
<div className="throughput-value">
<span className="value">128</span> Units/hour
</div>
{/* Progress Bar */}
<div className="progress-bar-wrapper">
{[...Array(totalBars)].map((_, i) => (
<div className="progress-bar" key={i}>
{i < barsToFill ? (
<div className="bar-fill full" />
) : i === barsToFill ? (
<div
className="bar-fill partial"
style={{ width: `${partialFillPercent}%` }}
/>
) : null}
</div>
))}
</div>
</div>
<div className="metrics-section">
<div className="metric">
<span className="label">Avg. Process Time</span>
<span className="value">28.4 Secs/unit</span>
</div>
<div className="metric">
<span className="label">Machine Utilization</span>
<span className="value">78%</span>
</div>
</div>
</div>
</div>
);
};
export default ProductionCapacity;

View File

@ -0,0 +1,22 @@
import React from "react";
import { ROISummaryIcon } from "../../icons/analysis";
const ROISummary = () => {
return (
<div className="analysis-card">
<div className="throughoutSummary-wrapper analysis-card-wrapper">
<div className="card-header">
<div className="header">
<div className="main-header">ROI Summary</div>
<div className="sub-header">From 24 November, 2025</div>
</div>
<div className="icon-wrapper">
<ROISummaryIcon />
</div>
</div>
</div>
</div>
);
};
export default ROISummary;

View File

@ -0,0 +1,146 @@
import React from "react";
import { Line } from "react-chartjs-2";
import {
Chart as ChartJS,
LineElement,
CategoryScale,
LinearScale,
PointElement,
} from "chart.js";
import { PowerIcon, ThroughputSummaryIcon } from "../../icons/analysis";
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement);
const ThroughputSummary = () => {
const data = {
labels: ["08:00", "08:10", "08:20", "08:30", "08:40", "08:50", "09:00"],
datasets: [
{
label: "Units/hour",
data: [100, 120, 110, 130, 125, 128, 132],
borderColor: "#B392F0",
tension: 0.4,
pointRadius: 0, // hide points
},
],
};
const options = {
responsive: true,
scales: {
x: {
grid: {
display: false,
},
ticks: {
display: false,
color: "#fff",
},
},
y: {
grid: {
display: false,
},
ticks: {
display: false,
color: "#fff",
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: true,
},
},
};
const shiftUtilization = {
"shift 1": 25,
"shift 2": 45,
"shift 3": 15,
};
return (
<div className="throughoutSummary analysis-card">
<div className="throughoutSummary-wrapper analysis-card-wrapper">
<div className="card-header">
<div className="header">
<div className="main-header">Throughput Summary</div>
<div className="sub-header">08:00 - 09:00 AM</div>
</div>
<div className="icon-wrapper">
<ThroughputSummaryIcon />
</div>
</div>
<div className="process-container">
<div className="throughput-value">
<span className="value">1240</span> Units/hour
</div>
<div className="lineChart">
<div className="assetUsage">
<div className="key">Asset usage</div>
<div className="value">85%</div>
</div>
<Line data={data} options={options} />
</div>
</div>
<div className="footer">
<div className="energyConsumption footer-card">
<div className="header">Energy Consumption</div>
<div className="value-container">
<div className="energy-icon">
<PowerIcon />
</div>
<div className="value-wrapper">
<div className="value">456</div>
<div className="unit">KWH</div>
</div>
</div>
</div>
<div className="shiftUtilization footer-card">
<div className="header">Shift Utilization</div>
<div className="value-container">
<div className="value">85%</div>
<div className="progress-wrapper">
<div
className="progress shift-1"
style={{ width: "30%" }}
></div>
<div
className="progress shift-2"
style={{ width: "40%" }}
></div>
<div
className="progress shift-3"
style={{ width: "30%" }}
></div>
</div>
<div className="progress-indicator">
<div className="shift-wrapper">
<span className="indicator shift-1"></span>
<label>Shift 1</label>
</div>
<div className="shift-wrapper">
<span className="indicator shift-2"></span>
<label>Shift 2</label>
</div>
<div className="shift-wrapper">
<span className="indicator shift-3"></span>
<label>Shift 3</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ThroughputSummary;

View File

@ -0,0 +1,54 @@
import React, { useRef } from "react";
import * as THREE from "three";
import { ThreeEvent } from "@react-three/fiber";
interface PickDropProps {
position: number[];
modelUuid: string;
pointUuid: string;
actionType: "pick" | "drop";
actionUuid: string;
gltfScene: THREE.Group;
selectedPoint: THREE.Mesh | null;
handlePointerDown: (e: ThreeEvent<PointerEvent>) => void;
isSelected: boolean;
}
const PickDropPoints: React.FC<PickDropProps> = ({
position,
modelUuid,
pointUuid,
actionType,
actionUuid,
gltfScene,
selectedPoint,
handlePointerDown,
isSelected,
}) => {
const groupRef = useRef<THREE.Group>(null);
return (
<group
ref={groupRef}
position={
Array.isArray(position) && position.length === 3
? new THREE.Vector3(...position)
: new THREE.Vector3(0, 0, 0)
}
onPointerDown={(e) => {
e.stopPropagation(); // Important to 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
scale={[0.5, 0.5, 0.5]}
/>
</group>
);
};
export default PickDropPoints;

View File

@ -0,0 +1,131 @@
import { useRef } 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 activeObjRef = useRef<THREE.Object3D | null>(null);
const planeRef = useRef<THREE.Plane>(
new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
);
const offsetRef = useRef<THREE.Vector3>(new THREE.Vector3());
const initialPositionRef = useRef<THREE.Vector3>(new THREE.Vector3());
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
const handlePointerDown = (e: ThreeEvent<PointerEvent>) => {
e.stopPropagation();
let obj: THREE.Object3D | null = e.object;
// Traverse up until we find modelUuid in userData
while (obj && !obj.userData?.modelUuid) {
obj = obj.parent;
}
if (!obj) return;
// Disable orbit controls while dragging
if (controls) (controls as any).enabled = false;
activeObjRef.current = obj;
initialPositionRef.current.copy(obj.position);
// Get world position
const objectWorldPos = new THREE.Vector3();
obj.getWorldPosition(objectWorldPos);
// Set plane at the object's Y level
planeRef.current.set(new THREE.Vector3(0, 1, 0), -objectWorldPos.y);
// Convert pointer to NDC
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;
// Raycast to intersection
raycaster.setFromCamera(pointer, camera);
const intersection = new THREE.Vector3();
raycaster.ray.intersectPlane(planeRef.current, intersection);
// Calculate offset
offsetRef.current.copy(objectWorldPos).sub(intersection);
// Start listening for drag
gl.domElement.addEventListener("pointermove", handlePointerMove);
gl.domElement.addEventListener("pointerup", handlePointerUp);
};
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);
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();
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
);
} 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);
};
const handlePointerUp = () => {
if (controls) (controls as any).enabled = true;
if (activeObjRef.current) {
// Pass the updated position to the onUpdate callback to persist it
onUpdate(activeObjRef.current);
}
gl.domElement.removeEventListener("pointermove", handlePointerMove);
gl.domElement.removeEventListener("pointerup", handlePointerUp);
activeObjRef.current = null;
};
return { handlePointerDown };
}

View File

@ -56,6 +56,7 @@ import ZoneGroup from "./groups/zoneGroup";
import useModuleStore from "../../store/useModuleStore";
import MeasurementTool from "../scene/tools/measurementTool";
import NavMesh from "../simulation/vehicle/navMesh/navMesh";
import ProductionCapacity from "../../components/ui/analysis/ProductionCapacity";
export default function Builder() {
const state = useThree<Types.ThreeState>(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements.
@ -110,7 +111,8 @@ export default function Builder() {
const cursorPosition = new THREE.Vector3(); // 3D vector for storing the cursor position.
const [selectedItemsIndex, setSelectedItemsIndex] = useState<Types.Number | null>(null); // State for tracking the index of the selected item.
const [selectedItemsIndex, setSelectedItemsIndex] =
useState<Types.Number | null>(null); // State for tracking the index of the selected item.
const { activeLayer, setActiveLayer } = useActiveLayer(); // State that changes based on which layer the user chooses in Layers.jsx.
const { toggleView, setToggleView } = useToggleView(); // State for toggling between 2D and 3D.
const { toolMode, setToolMode } = useToolMode();
@ -349,7 +351,6 @@ export default function Builder() {
<MeasurementTool />
<NavMesh lines={lines} />
</>
);
}

View File

@ -1,28 +1,182 @@
import React, { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { useEventsStore } from '../../../../../store/simulation/useEventsStore';
import useModuleStore from '../../../../../store/useModuleStore';
import { TransformControls } from '@react-three/drei';
import { detectModifierKeys } from '../../../../../utils/shortcutkeys/detectModifierKeys';
import { useSelectedEventSphere, useSelectedEventData } from '../../../../../store/simulation/useSimulationStore';
import React, { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { useEventsStore } from "../../../../../store/simulation/useEventsStore";
import useModuleStore from "../../../../../store/useModuleStore";
import { TransformControls, useGLTF } from "@react-three/drei";
import { detectModifierKeys } from "../../../../../utils/shortcutkeys/detectModifierKeys";
import {
useSelectedEventSphere,
useSelectedEventData,
} from "../../../../../store/simulation/useSimulationStore";
import PickDropPoints from "../../../../../components/ui/arm/PickDropPoints";
import armPick from "../../../../../assets/gltf-glb/arm_ui_pick.glb";
import armDrop from "../../../../../assets/gltf-glb/arm_ui_drop.glb";
import useDraggableGLTF from "../../../../../components/ui/arm/useDraggableGLTF";
interface Process {
startPoint: number[] | null;
endPoint: number[] | null;
}
interface Action {
actionUuid: string;
actionName: string;
actionType: string;
process: Process;
triggers: any[];
}
interface Point {
uuid: string;
position: number[];
rotation: number[];
actions: Action[];
}
interface RoboticArmEvent {
modelUuid: string;
modelName: string;
position: number[];
rotation: number[];
state: string;
type: string;
speed: number;
point: Point;
}
function PointsCreator() {
const { events, updatePoint, getPointByUuid, getEventByModelUuid } = useEventsStore();
const { events, updatePoint, getPointByUuid, getEventByModelUuid } =
useEventsStore();
const [armBotStatusSample, setArmBotStatusSample] = useState<
RoboticArmEvent[]
>([
{
modelUuid: "b3556818-9e46-48b0-a869-a75c92857125",
modelName: "robotic_arm",
position: [0, 0, 0],
rotation: [0, 0, 0],
state: "idle",
type: "roboticArm",
speed: 1.5,
point: {
uuid: "point-123",
position: [0, 1.5, 0],
rotation: [0, 0, 0],
actions: [
{
actionUuid: "action-001",
actionName: "Pick Component",
actionType: "pickAndPlace",
process: {
startPoint: null,
endPoint: null,
},
triggers: [],
},
{
actionUuid: "action-002",
actionName: "Pick Component",
actionType: "pickAndPlace",
process: {
startPoint: null,
endPoint: null,
},
triggers: [],
},
],
},
},
{
modelUuid: "16a394c7-0808-4bdf-a5d3-e5ca141ffb9f",
modelName: "arm without rig (1)",
position: [0, 0, 0],
rotation: [0, 0, 0],
state: "idle",
type: "roboticArm",
speed: 1.5,
point: {
uuid: "point-123",
position: [0, 1.5, 0],
rotation: [0, 0, 0],
actions: [
{
actionUuid: "action-001",
actionName: "Pick Component",
actionType: "pickAndPlace",
process: {
startPoint: null,
endPoint: null,
},
triggers: [],
},
],
},
},
]);
const armUiPick = useGLTF(armPick) as any;
const armUiDrop = useGLTF(armDrop) as any;
const updatePointToState = (obj: THREE.Object3D) => {
const { modelUuid, pointUuid, actionType, actionUuid } = obj.userData;
const newPosition = obj.position.toArray();
setArmBotStatusSample((prev) =>
prev.map((event) => {
if (event.modelUuid === modelUuid) {
const updatedActions = event.point.actions.map((action) => {
if (action.actionUuid === actionUuid) {
const updatedProcess = { ...action.process };
if (actionType === "pick") {
updatedProcess.startPoint = newPosition;
} else if (actionType === "drop") {
updatedProcess.endPoint = newPosition;
}
return {
...action,
process: updatedProcess,
};
}
return action;
});
return {
...event,
point: {
...event.point,
actions: updatedActions,
},
};
}
return event;
})
);
};
const { handlePointerDown } = useDraggableGLTF(updatePointToState);
const { activeModule } = useModuleStore();
const transformRef = useRef<any>(null);
const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null);
const [transformMode, setTransformMode] = useState<
"translate" | "rotate" | null
>(null);
const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({});
const { selectedEventSphere, setSelectedEventSphere, clearSelectedEventSphere } = useSelectedEventSphere();
const { setSelectedEventData, clearSelectedEventData } = useSelectedEventData();
const {
selectedEventSphere,
setSelectedEventSphere,
clearSelectedEventSphere,
} = useSelectedEventSphere();
const { setSelectedEventData, clearSelectedEventData } =
useSelectedEventData();
useEffect(() => {
if (selectedEventSphere) {
const eventData = getEventByModelUuid(selectedEventSphere.userData.modelUuid);
if (eventData) {
setSelectedEventData(
eventData,
selectedEventSphere.userData.pointUuid
const eventData = getEventByModelUuid(
selectedEventSphere.userData.modelUuid
);
if (eventData) {
setSelectedEventData(eventData, selectedEventSphere.userData.pointUuid);
} else {
clearSelectedEventData();
}
@ -47,109 +201,123 @@ function PointsCreator() {
return () => window.removeEventListener("keydown", handleKeyDown);
}, [selectedEventSphere]);
const updatePointToState = (selectedEventSphere: THREE.Mesh) => {
let point = JSON.parse(JSON.stringify(getPointByUuid(selectedEventSphere.userData.modelUuid, selectedEventSphere.userData.pointUuid)));
if (point) {
point.position = [selectedEventSphere.position.x, selectedEventSphere.position.y, selectedEventSphere.position.z];
updatePoint(selectedEventSphere.userData.modelUuid, selectedEventSphere.userData.pointUuid, point)
const [selectedPoint, setSelectedPoint] = useState<THREE.Mesh | null>(null);
const getDefaultPositions = (modelUuid: string) => {
const modelData = getModelByUuid(modelUuid);
if (modelData) {
const baseX = modelData.position?.[0] || 0;
const baseY = modelData.position?.[1] + 2.8 || 1.5;
const baseZ = modelData.position?.[2] || 0;
return {
pick: [baseX, baseY, baseZ - 2.5],
drop: [baseX, baseY, baseZ - 0.5],
default: [baseX, baseY, baseZ - 1.5],
};
}
return {
pick: [0.5, 1.5, 0],
drop: [-0.5, 1.5, 0],
default: [0, 1.5, 0],
};
};
const getModelByUuid = (modelUuid: string) => {
try {
const modelsJson = localStorage.getItem("FloorItems");
if (modelsJson) {
const models = JSON.parse(modelsJson);
return models.find((m: any) => m.modeluuid === modelUuid);
}
const storeModels = (useModuleStore.getState() as any).models || [];
return storeModels.find((m: any) => m.modelUuid === modelUuid);
} catch (error) {}
return null;
};
useEffect(() => {
console.log("armBotStatusSample: ", armBotStatusSample);
}, [armBotStatusSample]);
return (
<>
{activeModule === "simulation" && (
<>
<group name="EventPointsGroup">
{armBotStatusSample.map((event, i) => {
if (event.type === "roboticArm") {
const defaultPositions = getDefaultPositions(event.modelUuid);
const isSelected =
selectedPoint?.userData?.modelUuid === event.modelUuid;
return (
<>
{activeModule === 'simulation' &&
<>
<group name='EventPointsGroup' >
{events.map((event, i) => {
if (event.type === 'transfer') {
return (
<group key={i} position={new THREE.Vector3(...event.position)}>
{event.points.map((point, j) => (
<mesh
uuid={point.uuid}
ref={(el) => (sphereRefs.current[point.uuid] = el!)}
onClick={(e) => {
e.stopPropagation();
setSelectedEventSphere(sphereRefs.current[point.uuid]);
}}
onPointerMissed={() => {
clearSelectedEventSphere();
setTransformMode(null);
}}
key={`${i}-${j}`}
position={new THREE.Vector3(...point.position)}
userData={{ modelUuid: event.modelUuid, pointUuid: point.uuid }}
<group
key={i}
position={new THREE.Vector3(...event.position)}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="orange" />
</mesh>
))}
</group>
);
} else if (event.type === 'vehicle') {
return (
<group key={i} position={new THREE.Vector3(...event.position)}>
<mesh
uuid={event.point.uuid}
ref={(el) => (sphereRefs.current[event.point.uuid] = el!)}
onClick={(e) => {
e.stopPropagation();
setSelectedEventSphere(sphereRefs.current[event.point.uuid]);
}}
onPointerMissed={() => {
clearSelectedEventSphere();
setTransformMode(null);
}}
position={new THREE.Vector3(...event.point.position)}
userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="blue" />
</mesh>
</group>
setSelectedEventSphere(
sphereRefs.current[event.point.uuid]
);
} else if (event.type === 'roboticArm') {
return (
<group key={i} position={new THREE.Vector3(...event.position)}>
<mesh
uuid={event.point.uuid}
ref={(el) => (sphereRefs.current[event.point.uuid] = el!)}
onClick={(e) => {
e.stopPropagation();
setSelectedEventSphere(sphereRefs.current[event.point.uuid]);
setSelectedPoint(e.object as THREE.Mesh);
}}
onPointerMissed={() => {
clearSelectedEventSphere();
setTransformMode(null);
}}
position={new THREE.Vector3(...event.point.position)}
userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }}
position={new THREE.Vector3(...defaultPositions.default)}
userData={{
modelUuid: event.modelUuid,
pointUuid: event.point.uuid,
}}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="green" />
<meshStandardMaterial
color={isSelected ? "yellow" : "green"}
/>
</mesh>
</group>
{event.point.actions.map((action) => {
if (action.actionType === "pickAndPlace") {
const pickPosition =
action.process.startPoint || defaultPositions.pick;
const dropPosition =
action.process.endPoint || defaultPositions.drop;
return (
<React.Fragment key={action.actionUuid}>
{/* Pick Point */}
<PickDropPoints
position={pickPosition}
modelUuid={event.modelUuid}
pointUuid={event.point.uuid}
actionType="pick"
actionUuid={action.actionUuid}
gltfScene={armUiPick.scene}
selectedPoint={selectedPoint}
handlePointerDown={handlePointerDown}
isSelected={isSelected}
/>
{/* Drop Point */}
<PickDropPoints
position={dropPosition}
modelUuid={event.modelUuid}
pointUuid={event.point.uuid}
actionType="drop"
actionUuid={action.actionUuid}
gltfScene={armUiDrop.scene}
selectedPoint={selectedPoint}
handlePointerDown={handlePointerDown}
isSelected={isSelected}
/>
</React.Fragment>
);
} else if (event.type === 'machine') {
return (
<group key={i} position={new THREE.Vector3(...event.position)}>
<mesh
uuid={event.point.uuid}
ref={(el) => (sphereRefs.current[event.point.uuid] = el!)}
onClick={(e) => {
e.stopPropagation();
setSelectedEventSphere(sphereRefs.current[event.point.uuid]);
}}
onPointerMissed={() => {
clearSelectedEventSphere();
setTransformMode(null);
}}
position={new THREE.Vector3(...event.point.position)}
userData={{ modelUuid: event.modelUuid, pointUuid: event.point.uuid }}
>
<sphereGeometry args={[0.1, 16, 16]} />
<meshStandardMaterial color="purple" />
</mesh>
}
return null;
})}
</group>
);
} else {
@ -157,13 +325,13 @@ function PointsCreator() {
}
})}
</group>
{(selectedEventSphere && transformMode) &&
<TransformControls ref={transformRef} object={selectedEventSphere} mode={transformMode} onMouseUp={(e) => { updatePointToState(selectedEventSphere) }} />
}
</>
}
)}
</>
);
}
export default PointsCreator;

View File

@ -23,6 +23,9 @@ import SimulationPlayer from "../components/ui/simulation/simulationPlayer";
import RenderOverlay from "../components/templates/Overlay";
import MenuBar from "../components/ui/menu/menu";
import KeyPressListener from "../utils/shortcutkeys/handleShortcutKeys";
import ProductionCapacity from "../components/ui/analysis/ProductionCapacity";
import ThroughputSummary from "../components/ui/analysis/ThroughputSummary";
import ROISummary from "../components/ui/analysis/ROISummary";
const Project: React.FC = () => {
let navigate = useNavigate();
@ -38,7 +41,7 @@ const Project: React.FC = () => {
setFloorItems([]);
setWallItems([]);
setZones([]);
setActiveModule('builder')
setActiveModule("builder");
const email = localStorage.getItem("email");
if (email) {
const Organization = email!.split("@")[1].split(".")[0];
@ -57,6 +60,11 @@ const Project: React.FC = () => {
return (
<div className="project-main">
{/* <div className="analysis">
<ProductionCapacity />
<ThroughputSummary />
<ROISummary />
</div> */}
<KeyPressListener />
{loadingProgress && <LoadingPage progress={loadingProgress} />}
{!isPlaying && (

View File

@ -0,0 +1,270 @@
.analysis {
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
z-index: 100000000000000000000000000000;
}
.analysis-card {
min-width: 333px;
// background: var(--primary-color);
border-radius: 20px;
padding: 8px;
.analysis-card-wrapper {
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;
height: 4px;
border-radius: 13px;
overflow: hidden;
background-color: #FBEBD7;
.bar-fill {
position: absolute;
height: 100%;
top: 0;
left: 0;
background-color: #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-color: 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-color: #F3C64D;
}
&:nth-child(2) {
background-color: #67B3F4;
}
&:nth-child(3) {
background-color: #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-color: #F3C64D;
}
}
&:nth-child(2) {
.indicator {
background-color: #67B3F4;
}
}
&:nth-child(3) {
.indicator {
background-color: #7981F5;
}
}
label {
font-size: var(--font-size-small);
position: relative;
}
.indicator {
display: inline-block;
width: 5px;
height: 5px;
border-radius: 50%;
}
}
}
}
}
}
}
}

View File

@ -25,6 +25,7 @@
@use 'components/simulation/simulation';
@use 'components/menu/menu';
@use 'components/confirmationPopUp';
@use 'components/analysis/analysis';
// layout
@use 'layout/loading';