From 3ebadf3c107f126a7b9aef73bf274d2bdcc1d618 Mon Sep 17 00:00:00 2001 From: Nalvazhuthi Date: Mon, 28 Apr 2025 12:26:31 +0530 Subject: [PATCH] added arm ui --- app/package-lock.json | 66 +-- app/src/assets/gltf-glb/arm_ui_drop.glb | Bin 0 -> 5320 bytes app/src/assets/gltf-glb/arm_ui_pick.glb | Bin 0 -> 5180 bytes app/src/components/icons/analysis.tsx | 93 +++ .../ui/analysis/ProductionCapacity.tsx | 62 ++ app/src/components/ui/analysis/ROISummary.tsx | 22 + .../ui/analysis/ThroughputSummary.tsx | 146 +++++ app/src/components/ui/arm/PickDropPoints.tsx | 54 ++ app/src/components/ui/arm/useDraggableGLTF.ts | 131 ++++ app/src/modules/builder/builder.tsx | 559 +++++++++--------- .../events/points/creator/pointsCreator.tsx | 488 ++++++++++----- app/src/pages/Project.tsx | 10 +- .../styles/components/analysis/analysis.scss | 270 +++++++++ app/src/styles/main.scss | 1 + 14 files changed, 1413 insertions(+), 489 deletions(-) create mode 100644 app/src/assets/gltf-glb/arm_ui_drop.glb create mode 100644 app/src/assets/gltf-glb/arm_ui_pick.glb create mode 100644 app/src/components/icons/analysis.tsx create mode 100644 app/src/components/ui/analysis/ProductionCapacity.tsx create mode 100644 app/src/components/ui/analysis/ROISummary.tsx create mode 100644 app/src/components/ui/analysis/ThroughputSummary.tsx create mode 100644 app/src/components/ui/arm/PickDropPoints.tsx create mode 100644 app/src/components/ui/arm/useDraggableGLTF.ts create mode 100644 app/src/styles/components/analysis/analysis.scss diff --git a/app/package-lock.json b/app/package-lock.json index fa04c9f..3d5aea4 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -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" } diff --git a/app/src/assets/gltf-glb/arm_ui_drop.glb b/app/src/assets/gltf-glb/arm_ui_drop.glb new file mode 100644 index 0000000000000000000000000000000000000000..2a75e7e8dbec084d49fdba2b99025f31cc97be28 GIT binary patch literal 5320 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 literal 0 HcmV?d00001 diff --git a/app/src/assets/gltf-glb/arm_ui_pick.glb b/app/src/assets/gltf-glb/arm_ui_pick.glb new file mode 100644 index 0000000000000000000000000000000000000000..979ade4a56367b6e788d02e6cfabeaced8af66e7 GIT binary patch literal 5180 zcmdT{dpuO@yMNbO<1&W9xFoqulaOX>#*DcsMx@9sQA)+cC}A*$T#}VYkw`hIpm2`*Qv`=j?ONUuR}MGwc1n-{*av=li_Rns;r9 z@O6O!0E=}2xP=42ZMBzYGDQ#@E0j=p$&@WZkx(p^CBimThC@EBmFhL>|DWF)tSYZf-7eHCL!Y439 zEC`MYj1OaJ0oJ9&GLUEWN;%i(XQE{6igsa0s6q!lHaY8Jbuc2-c3xy)gZ}eudhfpGj zhzJYziHh5@RfMUdD8*($tk5YcB1-Hc2%aej78%Wk!{yTHSae)EgTdgku*g_UmJNf+ z<#6dNmW?f!gF!SK8!ne&L$jsRZRuPtjZ39tDnx$s^2d6ZfZ{(K{)t)JrYICs=rkkX zpUVDCsc3OnWSAr@{u@Cck%+@K$4L~!WQw=fYFA%ZFV7j3J-vK99Nj5AI+fzP%-_k& z%f~s8M&a3{{JruDtN&t`D5QuB4Hb&lgbBCLtopxkn1Z>)DjgQAh?0RN_^nDa3r1qQ zg~DT|%+ya1jB#S4#9!;D*soC{p-AGJ7>#jk={77XB{(WhgtZHkiCsqu60l7}x8`za zHcYw=m&0P)+A`Qw46$X>7)&OU&BDsXVPFV0$7XYEvAWr^ZMilah0tMG_XK=Lx3;lm za+nxy!@;I76Y6(ZkJ)mVY$gL6+Hx5*I){q!zOI2SLu0e(Y&sWPLoyQ@Wz8}t2G)bg zSa|M@ioBzZECh0d-=u8&#&zM#_Il4RgP!wD0uOfa`_Um{?|ILXKp}SDDMS}Gv z1IrX6V~AI1=*(S-_LucH%l}BX!Rkidpb5ZIS5M&VR)<}KtI+ne)^my+$x9Ou{@ac0?&ET`COe`FdaI8P%kS?J6c zGL7W~0G1g9@CX1{lGqP~KnNZNij*OsiS)r{nhS6UUG25T3Drc^lLRH)Noax=*lifw zKRwj{>m1Yd0}orx%8uRSxK|D(G}IjEOa9nyUTRP>RAJ@dxHdC$v$wo_c=_j^A$Mw$ zULSv3(n!pKCOF&Z#zltuySzZllB)rJ9Kv4U@%=ol;<=^=@2ALLp1)R{(pxn7K~vgM z7PN=xIcRUxHG9v;yjBK@Eq+3Lg~B$Oo~H_vRFdY=`>o1`o95r0b7L~c=xmVLpwCf% zFV%hdUJpOyytjKum<=Q?m%!uB;dM^6A3ZLfq%{cx71z9HS}(VDE`*+NQrl0w>%br%iql( z5eCFO`Q)dYdAI&dY5Mw~FhnT=?)hW^4fc6Ek1C-eodjXT#qIWn+s54@yUa#%w9-`0 zU07bQg!}tb`rvFE@T^+^k$>DjW^Bh;;caBCFItV?(Gl7`o*yx~^4gwO@-(}%$@R%f zDPmVKXRhCdrh_RvU60H~&=x;^U^mbaKJ;kEJk~Q(U5szXrbm~HsSlIZUiYcUd`_({ z`c4#bVd6+}!C|gmUk+;-SGvg`C4W34pf ziF}ubaiN6k^CBwqAo2?>0yvt{Fwz`)Bdb4;8-3iZr-n6f?MjsNX;}|NnVb7F%k$kP zym3PGAf+t7xUlCvVGh<=!$tr|-?wjYf^@fCIvJa1v|JDNPtM`Xt`whFt$o?(Hfw_? zE-1f_!gmdD6}8d2_kO_xfU=Z;jJ;N_6%oBqve#A>>(blU*{aq`zCqKdb{ra1e!iky z3j{8xl5$YDcyVbjZj*}gH5x4oo z5MEYb4~CclRg1&Gt1K0;FjhH;o_(rfG_jngDOSmF= zoij9!Lee`b=&=W@I;+5=wuYokiZ6I&0wjk1zMl8pC1Is30wGbHG2V~|}AQQ#QjGsX}F5;z`hBU#7M>YCE z8kOC+UL<8z)n3mkI2AP3o*+^hY@dQ?r!N!c<7h>>}iSf&ezd%KW@d zP&9cNIB|iRl|6heNetPESJ$?#)R!eA1*Axd6`vLtzQ+q!r>z6d8^OTH5)@^t0ZI^z z-ac}TJI@3ZMnY%?A5^OOUeRSDdH!ar@(gg`CD-c2deGYKAN4+s@cDzrZ*Y7}QA58Y zxL9SWcDDtXr!ln;=0RZVQSfZn)Rp1);JO?_ubu-yWCDut%KQ&-_WiKN)&0HDPe!K` z0?DtyZ$+iW;f}tC`{R!2>(!GEz?O}3_km7lu&GNMT<0v**4zW5y3M4{yg(q|s|Rpy zIO%MCuzUU~SVdDhMt(F05vV+Ff}`aTtKRF&*6ZtzWTfNKjm8h}E#J$_CxVtbH30tr zq-z!fpra1nTAaG1tR(wM=~35QtUm#mb^xHQSbF2k+KsXxoskj=jJ{t%nBhy_eXQ#x6Kff*rIy6OpnxS>` z$E7t>IY-OPf~$1Y_u|B5PtP@`K!uQjV-a`zlKPWpLbfv9XWkK)?cl=tlXFSlX?pKB z!9SxN>KdcQICW!(=#n1Ulo#Yy1YZr&n0&33M|=vTW6+#Vg6eEdgJGhSSHV9~1^@U$ zZ8Xcy+p?O!?k)emKCT|?RVRqvBcq_>Jo0_Ga^6v6`Lg{Woeqy- zkE3Ua=Myx0z4oI}2I6={w8Q-AG;ukIk-#U3Z9$*Dsx2yCV}0Z{YsG(oJb!tf|+*A0BfXfA=pUHoja}xbshk`e$p<$ywz(zfQggvlM)o z8f|x3v>ZR48uS4fT?A4Mdz0zcb$$rlG@%ZeluQT;?(D{|GLAMtL^pe8VA{x)5vJMcHJOJ zU8hdxcc+*sQpQB!9^zJO)TAm{&f{!Xlq@G_hiI`u_einR7o+qy4U_&rH!r(;oZWyk zW5Uzed3PG?D>cO_R|Cn=UZspw#~>b%2aFwa@g4{r{{$+7{#9#F5vNZ)IzHq)d~{LU zjh-iGVjAvH!JO;MGSiA4Uo=VldHt`?_D75jo~E_@PK-BUA9@`4QChe3(6nt{h|*+0 z^y*fN5mMl;h3di8cOEyj`>rJ!27b7ZV-FnbYprS){9>8C&zzWpSbn%tD@sHK?M>@r zl=*b?ss9Nc7hAqp_skaNrc!A48jF+dotw~jCHdP>oii5bj`_zUUAG{Xn&34lsk2OO z!vxK(A7UieX60>!w2pP2d#ft9ULAT{(g|)Mor})rcP>E( zOnIpFXgmIuDyoNCgL$YL6`oY$&*m?Ov|z&flAC0BYtPO5>5V9rZ^_>WaIh*K%;#I8 z?O+G0#>WGKcb|zOdenXtQB&6>YxJsuUD`5Jpb5z@fyyVKZ6)wl=1<8!Ys%7grh}J+ z=@KVXo>TKa+0dI^E)aQnLjq2|6v(K6&ju1D`7A&K9DZFgz;cF$?(WQkvtf6*4QYc% z;c8VG!CxI%$V~FgfI=ufaRMl4f-j}eA+U+Ri0_S^ejRu-|6kYx^R literal 0 HcmV?d00001 diff --git a/app/src/components/icons/analysis.tsx b/app/src/components/icons/analysis.tsx new file mode 100644 index 0000000..3a5542b --- /dev/null +++ b/app/src/components/icons/analysis.tsx @@ -0,0 +1,93 @@ +export function ThroughputSummaryIcon() { + return ( + + + + + ); +} +export function ProductionCapacityIcon() { + return ( + + + + + ); +} +export function ROISummaryIcon() { + return ( + + + + + + ); +} +export function PowerIcon() { + return ( + + + + + + + + + + + + ); +} diff --git a/app/src/components/ui/analysis/ProductionCapacity.tsx b/app/src/components/ui/analysis/ProductionCapacity.tsx new file mode 100644 index 0000000..268ea81 --- /dev/null +++ b/app/src/components/ui/analysis/ProductionCapacity.tsx @@ -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 ( +
+
+
+
+
Throughput Summary
+
08:00 - 09:00 AM
+
+
+ +
+
+ +
+
+ 128 Units/hour +
+ + {/* Progress Bar */} +
+ {[...Array(totalBars)].map((_, i) => ( +
+ {i < barsToFill ? ( +
+ ) : i === barsToFill ? ( +
+ ) : null} +
+ ))} +
+
+ +
+
+ Avg. Process Time + 28.4 Secs/unit +
+
+ Machine Utilization + 78% +
+
+
+
+ ); +}; + +export default ProductionCapacity; diff --git a/app/src/components/ui/analysis/ROISummary.tsx b/app/src/components/ui/analysis/ROISummary.tsx new file mode 100644 index 0000000..331eaaf --- /dev/null +++ b/app/src/components/ui/analysis/ROISummary.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import { ROISummaryIcon } from "../../icons/analysis"; + +const ROISummary = () => { + return ( +
+
+
+
+
ROI Summary
+
From 24 November, 2025
+
+
+ +
+
+
+
+ ); +}; + +export default ROISummary; diff --git a/app/src/components/ui/analysis/ThroughputSummary.tsx b/app/src/components/ui/analysis/ThroughputSummary.tsx new file mode 100644 index 0000000..cb4fac5 --- /dev/null +++ b/app/src/components/ui/analysis/ThroughputSummary.tsx @@ -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 ( +
+
+
+
+
Throughput Summary
+
08:00 - 09:00 AM
+
+
+ +
+
+ +
+
+ 1240 Units/hour +
+
+
+
Asset usage
+
85%
+
+ +
+
+ +
+
+
Energy Consumption
+
+
+ +
+
+
456
+
KWH
+
+
+
+
+
Shift Utilization
+
+
85%
+ +
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+ ); +}; + +export default ThroughputSummary; diff --git a/app/src/components/ui/arm/PickDropPoints.tsx b/app/src/components/ui/arm/PickDropPoints.tsx new file mode 100644 index 0000000..4544a46 --- /dev/null +++ b/app/src/components/ui/arm/PickDropPoints.tsx @@ -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) => void; + isSelected: boolean; +} + +const PickDropPoints: React.FC = ({ + position, + modelUuid, + pointUuid, + actionType, + actionUuid, + gltfScene, + selectedPoint, + handlePointerDown, + isSelected, +}) => { + const groupRef = useRef(null); + + return ( + { + e.stopPropagation(); // Important to prevent event bubbling + if (!isSelected) return; + handlePointerDown(e); + }} + userData={{ modelUuid, pointUuid, actionType, actionUuid }} + > + + + ); +}; + +export default PickDropPoints; diff --git a/app/src/components/ui/arm/useDraggableGLTF.ts b/app/src/components/ui/arm/useDraggableGLTF.ts new file mode 100644 index 0000000..b7e9272 --- /dev/null +++ b/app/src/components/ui/arm/useDraggableGLTF.ts @@ -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(null); + const planeRef = useRef( + new THREE.Plane(new THREE.Vector3(0, 1, 0), 0) + ); + const offsetRef = useRef(new THREE.Vector3()); + const initialPositionRef = useRef(new THREE.Vector3()); + + const raycaster = new THREE.Raycaster(); + const pointer = new THREE.Vector2(); + + const handlePointerDown = (e: ThreeEvent) => { + 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 }; +} + + + diff --git a/app/src/modules/builder/builder.tsx b/app/src/modules/builder/builder.tsx index e5ff1c1..d367493 100644 --- a/app/src/modules/builder/builder.tsx +++ b/app/src/modules/builder/builder.tsx @@ -18,20 +18,20 @@ import Window from "../../assets/gltf-glb/window.glb"; ////////// Zustand State Imports ////////// import { - useToggleView, - useDeletePointOrLine, - useMovePoint, - useActiveLayer, - useSocketStore, - useWallVisibility, - useRoofVisibility, - useShadows, - useUpdateScene, - useWalls, - useToolMode, - useRefTextUpdate, - useRenderDistance, - useLimitDistance, + useToggleView, + useDeletePointOrLine, + useMovePoint, + useActiveLayer, + useSocketStore, + useWallVisibility, + useRoofVisibility, + useShadows, + useUpdateScene, + useWalls, + useToolMode, + useRefTextUpdate, + useRenderDistance, + useLimitDistance, } from "../../store/store"; ////////// 3D Function Imports ////////// @@ -56,300 +56,301 @@ 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(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. - const csg = useRef(); // Reference for CSG object, used for 3D modeling. - const CSGGroup = useRef() as Types.RefMesh; // Reference to a group of CSG objects. - const scene = useRef() as Types.RefScene; // Reference to the scene. - const camera = useRef() as Types.RefCamera; // Reference to the camera object. - const controls = useRef(); // Reference to the controls object. - const raycaster = useRef() as Types.RefRaycaster; // Reference for raycaster used for detecting objects being pointed at in the scene. - const dragPointControls = useRef() as Types.RefDragControl; // Reference for drag point controls, an array for drag control. + const state = useThree(); // Importing the state from the useThree hook, which contains the scene, camera, and other Three.js elements. + const csg = useRef(); // Reference for CSG object, used for 3D modeling. + const CSGGroup = useRef() as Types.RefMesh; // Reference to a group of CSG objects. + const scene = useRef() as Types.RefScene; // Reference to the scene. + const camera = useRef() as Types.RefCamera; // Reference to the camera object. + const controls = useRef(); // Reference to the controls object. + const raycaster = useRef() as Types.RefRaycaster; // Reference for raycaster used for detecting objects being pointed at in the scene. + const dragPointControls = useRef() as Types.RefDragControl; // Reference for drag point controls, an array for drag control. - // Assigning the scene and camera from the Three.js state to the references. + // Assigning the scene and camera from the Three.js state to the references. - scene.current = state.scene; - camera.current = state.camera; - controls.current = state.controls; - raycaster.current = state.raycaster; + scene.current = state.scene; + camera.current = state.camera; + controls.current = state.controls; + raycaster.current = state.raycaster; - const plane = useRef(null); // Reference for a plane object for raycaster reference. - const grid = useRef() as any; // Reference for a grid object for raycaster reference. - const snappedPoint = useRef() as Types.RefVector3; // Reference for storing a snapped point at the (end = isSnapped) and (start = ispreSnapped) of the line. - const isSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (end). - const anglesnappedPoint = useRef() as Types.RefVector3; // Reference for storing an angle-snapped point when the line is in 90 degree etc... - const isAngleSnapped = useRef(false) as Types.RefBoolean; // Boolean to indicate if angle snapping is active. - const isSnappedUUID = useRef() as Types.RefString; // UUID reference to identify the snapped point. - const ispreSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (start). - const tempLoader = useRef() as Types.RefMesh; // Reference for a temporary loader for the floor items. - const isTempLoader = useRef() as Types.RefBoolean; // Reference to check if a temporary loader is active. - const Tube = useRef() as Types.RefTubeGeometry; // Reference for tubes used for reference line creation and updation. - const line = useRef([]) as Types.RefLine; // Reference for line which stores the current line that is being drawn. - const lines = useRef([]) as Types.RefLines; // Reference for lines which stores all the lines that are ever drawn. - const onlyFloorline = useRef([]); // Reference for floor lines which does not have walls or roof and have only floor used to store the current line that is being drawn. - const onlyFloorlines = useRef([]); // Reference for all the floor lines that are ever drawn. - const ReferenceLineMesh = useRef() as Types.RefMesh; // Reference for storing the mesh of the reference line for moving it during draw. - const LineCreated = useRef(false) as Types.RefBoolean; // Boolean to track whether the reference line is created or not. - const referencePole = useRef() as Types.RefMesh; // Reference for a pole that is used as the reference for the user to show where it is placed. - const itemsGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the floor items (Gltf). - const floorGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the roofs and the floors. - const AttachedObject = useRef() as Types.RefMesh; // Reference for an object that is attached using dbl click for transform controls rotation. - const floorPlanGroup = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines group and the points group. - const floorPlanGroupLine = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines that are drawn. - const floorPlanGroupPoint = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the points that are created. - const floorGroupAisle = useRef() as Types.RefGroup; - const zoneGroup = useRef() as Types.RefGroup; - const currentLayerPoint = useRef([]) as Types.RefMeshArray; // Reference for points that re in the current layer used to update the points in drag controls. - const hoveredDeletablePoint = useRef() as Types.RefMesh; // Reference for the currently hovered point that can be deleted. - const hoveredDeletableLine = useRef() as Types.RefMesh; // Reference for the currently hovered line that can be deleted. - const hoveredDeletableFloorItem = useRef() as Types.RefMesh; // Reference for the currently hovered floor item that can be deleted. - const hoveredDeletableWallItem = useRef() as Types.RefMesh; // Reference for the currently hovered wall item that can be deleted. - const hoveredDeletablePillar = useRef() as Types.RefMesh; // Reference for the currently hovered pillar that can be deleted. - const currentWallItem = useRef() as Types.RefMesh; // Reference for the currently selected wall item that can be scaled, dragged etc... + const plane = useRef(null); // Reference for a plane object for raycaster reference. + const grid = useRef() as any; // Reference for a grid object for raycaster reference. + const snappedPoint = useRef() as Types.RefVector3; // Reference for storing a snapped point at the (end = isSnapped) and (start = ispreSnapped) of the line. + const isSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (end). + const anglesnappedPoint = useRef() as Types.RefVector3; // Reference for storing an angle-snapped point when the line is in 90 degree etc... + const isAngleSnapped = useRef(false) as Types.RefBoolean; // Boolean to indicate if angle snapping is active. + const isSnappedUUID = useRef() as Types.RefString; // UUID reference to identify the snapped point. + const ispreSnapped = useRef(false) as Types.RefBoolean; // Boolean reference to indicate if an object is snapped at the (start). + const tempLoader = useRef() as Types.RefMesh; // Reference for a temporary loader for the floor items. + const isTempLoader = useRef() as Types.RefBoolean; // Reference to check if a temporary loader is active. + const Tube = useRef() as Types.RefTubeGeometry; // Reference for tubes used for reference line creation and updation. + const line = useRef([]) as Types.RefLine; // Reference for line which stores the current line that is being drawn. + const lines = useRef([]) as Types.RefLines; // Reference for lines which stores all the lines that are ever drawn. + const onlyFloorline = useRef([]); // Reference for floor lines which does not have walls or roof and have only floor used to store the current line that is being drawn. + const onlyFloorlines = useRef([]); // Reference for all the floor lines that are ever drawn. + const ReferenceLineMesh = useRef() as Types.RefMesh; // Reference for storing the mesh of the reference line for moving it during draw. + const LineCreated = useRef(false) as Types.RefBoolean; // Boolean to track whether the reference line is created or not. + const referencePole = useRef() as Types.RefMesh; // Reference for a pole that is used as the reference for the user to show where it is placed. + const itemsGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the floor items (Gltf). + const floorGroup = useRef() as Types.RefGroup; // Reference to the THREE.Group that has the roofs and the floors. + const AttachedObject = useRef() as Types.RefMesh; // Reference for an object that is attached using dbl click for transform controls rotation. + const floorPlanGroup = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines group and the points group. + const floorPlanGroupLine = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the lines that are drawn. + const floorPlanGroupPoint = useRef() as Types.RefGroup; // Reference for a THREE.Group that has the points that are created. + const floorGroupAisle = useRef() as Types.RefGroup; + const zoneGroup = useRef() as Types.RefGroup; + const currentLayerPoint = useRef([]) as Types.RefMeshArray; // Reference for points that re in the current layer used to update the points in drag controls. + const hoveredDeletablePoint = useRef() as Types.RefMesh; // Reference for the currently hovered point that can be deleted. + const hoveredDeletableLine = useRef() as Types.RefMesh; // Reference for the currently hovered line that can be deleted. + const hoveredDeletableFloorItem = useRef() as Types.RefMesh; // Reference for the currently hovered floor item that can be deleted. + const hoveredDeletableWallItem = useRef() as Types.RefMesh; // Reference for the currently hovered wall item that can be deleted. + const hoveredDeletablePillar = useRef() as Types.RefMesh; // Reference for the currently hovered pillar that can be deleted. + const currentWallItem = useRef() as Types.RefMesh; // Reference for the currently selected wall item that can be scaled, dragged etc... - const cursorPosition = new THREE.Vector3(); // 3D vector for storing the cursor position. + const cursorPosition = new THREE.Vector3(); // 3D vector for storing the cursor position. - const [selectedItemsIndex, setSelectedItemsIndex] = useState(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(); - const { movePoint, setMovePoint } = useMovePoint(); // State that stores a boolean which represents whether the move mode is active or not. - const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); - const { socket } = useSocketStore(); - const { roofVisibility, setRoofVisibility } = useRoofVisibility(); - const { wallVisibility, setWallVisibility } = useWallVisibility(); - const { shadows, setShadows } = useShadows(); - const { renderDistance, setRenderDistance } = useRenderDistance(); - const { limitDistance, setLimitDistance } = useLimitDistance(); - const { updateScene, setUpdateScene } = useUpdateScene(); - const { walls, setWalls } = useWalls(); - const { refTextupdate, setRefTextUpdate } = useRefTextUpdate(); - const { activeModule } = useModuleStore(); + const [selectedItemsIndex, setSelectedItemsIndex] = + useState(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(); + const { movePoint, setMovePoint } = useMovePoint(); // State that stores a boolean which represents whether the move mode is active or not. + const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); + const { socket } = useSocketStore(); + const { roofVisibility, setRoofVisibility } = useRoofVisibility(); + const { wallVisibility, setWallVisibility } = useWallVisibility(); + const { shadows, setShadows } = useShadows(); + const { renderDistance, setRenderDistance } = useRenderDistance(); + const { limitDistance, setLimitDistance } = useLimitDistance(); + const { updateScene, setUpdateScene } = useUpdateScene(); + const { walls, setWalls } = useWalls(); + const { refTextupdate, setRefTextUpdate } = useRefTextUpdate(); + const { activeModule } = useModuleStore(); - // const loader = new GLTFLoader(); - // const dracoLoader = new DRACOLoader(); + // const loader = new GLTFLoader(); + // const dracoLoader = new DRACOLoader(); - // dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); - // loader.setDRACOLoader(dracoLoader); + // dracoLoader.setDecoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/draco/gltf/'); + // loader.setDRACOLoader(dracoLoader); - ////////// Assest Configuration Values ////////// + ////////// Assest Configuration Values ////////// - const AssetConfigurations: Types.AssetConfigurations = { - arch: { - modelUrl: arch, - scale: [0.75, 0.75, 0.75], - csgscale: [2, 4, 0.5], - csgposition: [0, 2, 0], - positionY: () => 0, - type: "Fixed-Move", - }, - door: { - modelUrl: door, - scale: [0.75, 0.75, 0.75], - csgscale: [2, 4, 0.5], - csgposition: [0, 2, 0], - positionY: () => 0, - type: "Fixed-Move", - }, - window: { - modelUrl: Window, - scale: [0.75, 0.75, 0.75], - csgscale: [5, 3, 0.5], - csgposition: [0, 1.5, 0], - positionY: (intersectionPoint) => intersectionPoint.point.y, - type: "Free-Move", - }, - }; + const AssetConfigurations: Types.AssetConfigurations = { + arch: { + modelUrl: arch, + scale: [0.75, 0.75, 0.75], + csgscale: [2, 4, 0.5], + csgposition: [0, 2, 0], + positionY: () => 0, + type: "Fixed-Move", + }, + door: { + modelUrl: door, + scale: [0.75, 0.75, 0.75], + csgscale: [2, 4, 0.5], + csgposition: [0, 2, 0], + positionY: () => 0, + type: "Fixed-Move", + }, + window: { + modelUrl: Window, + scale: [0.75, 0.75, 0.75], + csgscale: [5, 3, 0.5], + csgposition: [0, 1.5, 0], + positionY: (intersectionPoint) => intersectionPoint.point.y, + type: "Free-Move", + }, + }; - ////////// All Toggle's ////////// + ////////// All Toggle's ////////// - useEffect(() => { - setRefTextUpdate((prevUpdate: number) => prevUpdate - 1); - if (dragPointControls.current) { - dragPointControls.current.enabled = false; - } - if (toggleView) { - Layer2DVisibility( - activeLayer, - floorPlanGroup, - floorPlanGroupLine, - floorPlanGroupPoint, - currentLayerPoint, - dragPointControls - ); - } else { - setToolMode(null); - setDeletePointOrLine(false); - setMovePoint(false); - loadWalls(lines, setWalls); - setUpdateScene(true); - line.current = []; - } - }, [toggleView]); + useEffect(() => { + setRefTextUpdate((prevUpdate: number) => prevUpdate - 1); + if (dragPointControls.current) { + dragPointControls.current.enabled = false; + } + if (toggleView) { + Layer2DVisibility( + activeLayer, + floorPlanGroup, + floorPlanGroupLine, + floorPlanGroupPoint, + currentLayerPoint, + dragPointControls + ); + } else { + setToolMode(null); + setDeletePointOrLine(false); + setMovePoint(false); + loadWalls(lines, setWalls); + setUpdateScene(true); + line.current = []; + } + }, [toggleView]); - useEffect(() => { - THREE.Cache.clear(); - THREE.Cache.enabled = true; - }, []); + useEffect(() => { + THREE.Cache.clear(); + THREE.Cache.enabled = true; + }, []); - useEffect(() => { - const email = localStorage.getItem("email"); - const organization = email!.split("@")[1].split(".")[0]; + useEffect(() => { + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; - async function fetchVisibility() { - const visibility = await findEnvironment( - organization, - localStorage.getItem("userId")! - ); - if (visibility) { - setRoofVisibility(visibility.roofVisibility); - setWallVisibility(visibility.wallVisibility); - setShadows(visibility.shadowVisibility); - setRenderDistance(visibility.renderDistance); - setLimitDistance(visibility.limitDistance); - } - } - fetchVisibility(); - }, []); + async function fetchVisibility() { + const visibility = await findEnvironment( + organization, + localStorage.getItem("userId")! + ); + if (visibility) { + setRoofVisibility(visibility.roofVisibility); + setWallVisibility(visibility.wallVisibility); + setShadows(visibility.shadowVisibility); + setRenderDistance(visibility.renderDistance); + setLimitDistance(visibility.limitDistance); + } + } + fetchVisibility(); + }, []); - ////////// UseFrame is Here ////////// + ////////// UseFrame is Here ////////// - useFrame(() => { - if (toolMode) { - Draw( - state, - plane, - cursorPosition, - floorPlanGroupPoint, - floorPlanGroupLine, - snappedPoint, - isSnapped, - isSnappedUUID, - line, - lines, - ispreSnapped, - floorPlanGroup, - ReferenceLineMesh, - LineCreated, - setRefTextUpdate, - Tube, - anglesnappedPoint, - isAngleSnapped, - toolMode - ); - } - }); + useFrame(() => { + if (toolMode) { + Draw( + state, + plane, + cursorPosition, + floorPlanGroupPoint, + floorPlanGroupLine, + snappedPoint, + isSnapped, + isSnappedUUID, + line, + lines, + ispreSnapped, + floorPlanGroup, + ReferenceLineMesh, + LineCreated, + setRefTextUpdate, + Tube, + anglesnappedPoint, + isAngleSnapped, + toolMode + ); + } + }); - ////////// Return ////////// + ////////// Return ////////// - return ( - <> - + return ( + <> + - + - + - + - + - + - + - + - + - + - + - - - - ); + + + ); } diff --git a/app/src/modules/simulation/events/points/creator/pointsCreator.tsx b/app/src/modules/simulation/events/points/creator/pointsCreator.tsx index 1b3defa..f5e61d3 100644 --- a/app/src/modules/simulation/events/points/creator/pointsCreator.tsx +++ b/app/src/modules/simulation/events/points/creator/pointsCreator.tsx @@ -1,169 +1,337 @@ -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 { activeModule } = useModuleStore(); - const transformRef = useRef(null); - const [transformMode, setTransformMode] = useState<"translate" | "rotate" | null>(null); - const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({}); - const { selectedEventSphere, setSelectedEventSphere, clearSelectedEventSphere } = useSelectedEventSphere(); - const { setSelectedEventData, clearSelectedEventData } = useSelectedEventData(); + const { events, updatePoint, getPointByUuid, getEventByModelUuid } = + useEventsStore(); - useEffect(() => { - if (selectedEventSphere) { - const eventData = getEventByModelUuid(selectedEventSphere.userData.modelUuid); - if (eventData) { - setSelectedEventData( - eventData, - selectedEventSphere.userData.pointUuid - ); - } else { - clearSelectedEventData(); + 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, + }; } - } else { - clearSelectedEventData(); + return action; + }); + + return { + ...event, + point: { + ...event.point, + actions: updatedActions, + }, + }; } - }, [selectedEventSphere]); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - const keyCombination = detectModifierKeys(e); - if (!selectedEventSphere) return; - if (keyCombination === "G") { - setTransformMode((prev) => (prev === "translate" ? null : "translate")); - } - if (keyCombination === "R") { - setTransformMode((prev) => (prev === "rotate" ? null : "rotate")); - } - }; - - window.addEventListener("keydown", handleKeyDown); - 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) - } - } - - return ( - <> - {activeModule === 'simulation' && - <> - - {events.map((event, i) => { - if (event.type === 'transfer') { - return ( - - {event.points.map((point, j) => ( - (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 }} - > - - - - ))} - - ); - } else if (event.type === 'vehicle') { - return ( - - (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 }} - > - - - - - ); - } else if (event.type === 'roboticArm') { - return ( - - (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 }} - > - - - - - ); - } else if (event.type === 'machine') { - return ( - - (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 }} - > - - - - - ); - } else { - return null; - } - })} - - {(selectedEventSphere && transformMode) && - { updatePointToState(selectedEventSphere) }} /> - } - - } - + return event; + }) ); + }; + + const { handlePointerDown } = useDraggableGLTF(updatePointToState); + + const { activeModule } = useModuleStore(); + const transformRef = useRef(null); + const [transformMode, setTransformMode] = useState< + "translate" | "rotate" | null + >(null); + const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({}); + const { + selectedEventSphere, + setSelectedEventSphere, + clearSelectedEventSphere, + } = useSelectedEventSphere(); + const { setSelectedEventData, clearSelectedEventData } = + useSelectedEventData(); + + useEffect(() => { + if (selectedEventSphere) { + const eventData = getEventByModelUuid( + selectedEventSphere.userData.modelUuid + ); + if (eventData) { + setSelectedEventData(eventData, selectedEventSphere.userData.pointUuid); + } else { + clearSelectedEventData(); + } + } else { + clearSelectedEventData(); + } + }, [selectedEventSphere]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const keyCombination = detectModifierKeys(e); + if (!selectedEventSphere) return; + if (keyCombination === "G") { + setTransformMode((prev) => (prev === "translate" ? null : "translate")); + } + if (keyCombination === "R") { + setTransformMode((prev) => (prev === "rotate" ? null : "rotate")); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [selectedEventSphere]); + + const [selectedPoint, setSelectedPoint] = useState(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" && ( + <> + + {armBotStatusSample.map((event, i) => { + if (event.type === "roboticArm") { + const defaultPositions = getDefaultPositions(event.modelUuid); + const isSelected = + selectedPoint?.userData?.modelUuid === event.modelUuid; + + return ( + + (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(...defaultPositions.default)} + userData={{ + modelUuid: event.modelUuid, + pointUuid: event.point.uuid, + }} + > + + + + {event.point.actions.map((action) => { + if (action.actionType === "pickAndPlace") { + const pickPosition = + action.process.startPoint || defaultPositions.pick; + const dropPosition = + action.process.endPoint || defaultPositions.drop; + + return ( + + {/* Pick Point */} + + + {/* Drop Point */} + + + ); + } + return null; + })} + + ); + } else { + return null; + } + })} + + + )} + + ); } export default PointsCreator; + + + diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index 2daa091..3f6b4cc 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -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 (
+ {/*
+ + + +
*/} {loadingProgress && } {!isPlaying && ( diff --git a/app/src/styles/components/analysis/analysis.scss b/app/src/styles/components/analysis/analysis.scss new file mode 100644 index 0000000..bc33556 --- /dev/null +++ b/app/src/styles/components/analysis/analysis.scss @@ -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%; + + } + } + } + } + } + + } + + + } +} \ No newline at end of file diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss index 5e46dd4..c0d2431 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -25,6 +25,7 @@ @use 'components/simulation/simulation'; @use 'components/menu/menu'; @use 'components/confirmationPopUp'; +@use 'components/analysis/analysis'; // layout @use 'layout/loading';