diff --git a/app/package-lock.json b/app/package-lock.json index 81daa02..84bb185 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -23,6 +23,8 @@ "chart.js": "^4.4.8", "glob": "^11.0.0", "gsap": "^3.12.5", + "leva": "^0.10.0", + "mqtt": "^5.10.4", "postprocessing": "^6.36.4", "prompt-sync": "^4.2.0", "react": "^18.3.1", @@ -2010,7 +2012,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==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -2022,7 +2024,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2471,6 +2473,35 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==", + "license": "MIT" + }, + "node_modules/@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^0.7.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", + "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^0.5.3", + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3397,6 +3428,287 @@ } } }, + "node_modules/@radix-ui/primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", + "integrity": "sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz", + "integrity": "sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.0.tgz", + "integrity": "sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz", + "integrity": "sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-escape-keydown": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", + "integrity": "sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.2.tgz", + "integrity": "sha512-swu32idoCW7KA2VEiUZGBSu9nB6qwGdV6k6HYhUoOo3M1FFpD+VgLzUqtt3mwL1ssz7r2x8MggpLSQach2Xy/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.0.tgz", + "integrity": "sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", + "integrity": "sha512-avutXAFL1ehGvAXtPquu0YK5oz6ctS474iM3vNGQIkswrVhdrS52e3uoMQBzZhNRAIE0jBnUyXWNmSjGHhCFcw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.5.tgz", + "integrity": "sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.1", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "@radix-ui/react-visually-hidden": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", + "integrity": "sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.0.tgz", + "integrity": "sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.2.tgz", + "integrity": "sha512-DXGim3x74WgUv+iMNCF+cAo8xUHHeqvjx8zs7trKf+FkQKPQXLk2sX7Gx1ysH7Q76xCpZuxIJE7HLPxRE+Q+GA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-callback-ref": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.0.tgz", + "integrity": "sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", + "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz", + "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.2.tgz", + "integrity": "sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz", + "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@react-spring/animated": { "version": "9.7.5", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", @@ -3724,6 +4036,15 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, + "node_modules/@stitches/react": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@stitches/react/-/react-1.2.8.tgz", + "integrity": "sha512-9g9dWI4gsSVe8bNLlb+lMkBYsnIKCZTmvqvDG+Avnn69XfmHZKiaMrx7cgTaddq7aTPPmXiTsbFcUy0xgI4+wA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3942,6 +4263,26 @@ "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", @@ -4053,25 +4394,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==", - "dev": true + "devOptional": 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==", - "dev": true + "devOptional": 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==", - "dev": true + "devOptional": 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==", - "dev": true + "devOptional": true }, "node_modules/@turf/along": { "version": "7.1.0", @@ -6278,6 +6619,22 @@ "@types/react": "*" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.18.tgz", + "integrity": "sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -6385,9 +6742,10 @@ "integrity": "sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==" }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -6806,6 +7164,18 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "deprecated": "Use your platform's native atob() and btoa() methods instead" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -7299,6 +7669,15 @@ "node": ">=0.8" } }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -7331,6 +7710,15 @@ "node": ">= 4.0.0" } }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -7711,6 +8099,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", + "integrity": "sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -8362,6 +8778,12 @@ "node": ">= 6" } }, + "node_modules/commist": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", + "integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==", + "license": "MIT" + }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -8421,6 +8843,21 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, "node_modules/concaveman": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/concaveman/-/concaveman-1.2.1.tgz", @@ -8538,7 +8975,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==", - "dev": true + "devOptional": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -9403,7 +9840,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -10673,6 +11110,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter2": { "version": "6.4.7", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", @@ -10809,6 +11255,27 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -10879,6 +11346,19 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-unique-numbers": { + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz", + "integrity": "sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.1.0" + } + }, "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", @@ -10987,6 +11467,18 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-selector": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.5.0.tgz", + "integrity": "sha512-s8KNnmIDTBoD0p9uJ9uD0XY38SCeBOtj0UMXyQSLg1Ypfrfj8+dAvwsLjYQkQ2GjhVtp2HrnF5cJzMhBjfD8HA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -11128,6 +11620,15 @@ "is-callable": "^1.1.3" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -11500,6 +12001,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -11790,6 +12300,12 @@ "he": "bin/he" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, "node_modules/hls.js": { "version": "1.5.17", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.17.tgz", @@ -12417,6 +12933,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -12569,6 +13097,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -12763,6 +13303,15 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -14002,6 +14551,16 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-sdsl": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", + "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -14302,6 +14861,46 @@ "node": "> 0.8" } }, + "node_modules/leva": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/leva/-/leva-0.10.0.tgz", + "integrity": "sha512-RiNJWmeqQdKIeHuVXgshmxIHu144a2AMYtLxKf8Nm1j93pisDPexuQDHKNdQlbo37wdyDQibLjY9JKGIiD7gaw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-tooltip": "1.0.5", + "@stitches/react": "^1.2.8", + "@use-gesture/react": "^10.2.5", + "colord": "^2.9.2", + "dequal": "^2.0.2", + "merge-value": "^1.0.0", + "react-colorful": "^5.5.1", + "react-dropzone": "^12.0.0", + "v8n": "^1.3.3", + "zustand": "^3.6.9" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/leva/node_modules/zustand": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", + "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", + "license": "MIT", + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -14598,7 +15197,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -14650,6 +15249,21 @@ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, + "node_modules/merge-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/merge-value/-/merge-value-1.0.0.tgz", + "integrity": "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "is-extendable": "^1.0.0", + "mixin-deep": "^1.2.0", + "set-value": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -14791,6 +15405,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -14802,6 +15429,109 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mqtt": { + "version": "5.10.4", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.10.4.tgz", + "integrity": "sha512-wN+SuhT2/ZaG6NPxca0N6YtRivnMxk6VflxQUEeqDH4erKdj+wPAGhHmcTLzvqfE4sJRxrEJ+XJxUc0No0E7eQ==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.18", + "@types/ws": "^8.5.14", + "commist": "^3.2.0", + "concat-stream": "^2.0.0", + "debug": "^4.4.0", + "help-me": "^5.0.0", + "lru-cache": "^10.4.3", + "minimist": "^1.2.8", + "mqtt-packet": "^9.0.1", + "number-allocator": "^1.0.14", + "readable-stream": "^4.7.0", + "reinterval": "^1.1.0", + "rfdc": "^1.4.1", + "split2": "^4.2.0", + "worker-timers": "^7.1.8", + "ws": "^8.18.0" + }, + "bin": { + "mqtt": "build/bin/mqtt.js", + "mqtt_pub": "build/bin/pub.js", + "mqtt_sub": "build/bin/sub.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz", + "integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==", + "license": "MIT", + "dependencies": { + "bl": "^6.0.8", + "debug": "^4.3.4", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/mqtt/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/mqtt/node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -14960,6 +15690,16 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/number-allocator": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz", + "integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.3.0" + } + }, "node_modules/nwsapi": { "version": "2.2.13", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", @@ -16813,7 +17553,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, "engines": { "node": ">= 0.6.0" } @@ -17098,6 +17837,16 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-composer": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", @@ -17239,6 +17988,23 @@ "loose-envify": "^1.1.0" } }, + "node_modules/react-dropzone": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.1.0.tgz", + "integrity": "sha512-iBYHA1rbopIvtzokEX4QubO6qk5IF/x3BtKGu74rF2JkQDXnwC4uO/lHKpaw4PJIV6iIAYOlwLv2FpiGyqHNog==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.5.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", @@ -17604,6 +18370,12 @@ "regjsparser": "bin/parser" } }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==", + "license": "MIT" + }, "node_modules/relateurl": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", @@ -17791,8 +18563,7 @@ "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "dev": true + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" }, "node_modules/rimraf": { "version": "3.0.2", @@ -18352,6 +19123,30 @@ "node": ">= 0.4" } }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-value/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -18579,6 +19374,40 @@ "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-3.1.2.tgz", "integrity": "sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A==" }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -19808,7 +20637,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -19851,7 +20680,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, + "devOptional": true, "dependencies": { "acorn": "^8.11.0" }, @@ -19863,7 +20692,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/tsconfig-paths": { "version": "3.15.0", @@ -20090,6 +20919,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -20266,6 +21101,20 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz", + "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", @@ -20330,7 +21179,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==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "8.1.1", @@ -20350,6 +21199,12 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "node_modules/v8n": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/v8n/-/v8n-1.5.1.tgz", + "integrity": "sha512-LdabyT4OffkyXFCe9UT+uMkxNBs5rcTVuZClvxQr08D5TUgo1OFKkoT65qYRCsiKBl/usHjpXvP4hHMzzDRj3A==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -21150,6 +22005,40 @@ "workbox-core": "6.6.0" } }, + "node_modules/worker-timers": { + "version": "7.1.8", + "resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-7.1.8.tgz", + "integrity": "sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "worker-timers-broker": "^6.1.8", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-broker": { + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz", + "integrity": "sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^8.0.13", + "tslib": "^2.6.2", + "worker-timers-worker": "^7.0.71" + } + }, + "node_modules/worker-timers-worker": { + "version": "7.0.71", + "resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz", + "integrity": "sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -21349,7 +22238,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } diff --git a/app/package.json b/app/package.json index a3c3503..f1ea92a 100644 --- a/app/package.json +++ b/app/package.json @@ -1,5 +1,5 @@ { - "name": "dwinzo-app", + "name": "dwinzo-beta", "version": "0.1.0", "private": true, "dependencies": { @@ -18,6 +18,8 @@ "chart.js": "^4.4.8", "glob": "^11.0.0", "gsap": "^3.12.5", + "leva": "^0.10.0", + "mqtt": "^5.10.4", "postprocessing": "^6.36.4", "prompt-sync": "^4.2.0", "react": "^18.3.1", diff --git a/app/src/assets/floor/concreteFloorWorn001Diff2k.jpg b/app/src/assets/floor/concreteFloorWorn001Diff2k.jpg new file mode 100644 index 0000000..f8ffbd3 Binary files /dev/null and b/app/src/assets/floor/concreteFloorWorn001Diff2k.jpg differ diff --git a/app/src/assets/floor/concreteFloorWorn001NorGl2k.jpg b/app/src/assets/floor/concreteFloorWorn001NorGl2k.jpg new file mode 100644 index 0000000..896b67f Binary files /dev/null and b/app/src/assets/floor/concreteFloorWorn001NorGl2k.jpg differ diff --git a/app/src/assets/gltf-glb/arch.glb b/app/src/assets/gltf-glb/arch.glb new file mode 100644 index 0000000..52f44ef Binary files /dev/null and b/app/src/assets/gltf-glb/arch.glb differ diff --git a/app/src/assets/gltf-glb/camera face 2.gltf b/app/src/assets/gltf-glb/camera face 2.gltf new file mode 100644 index 0000000..e1c418f --- /dev/null +++ b/app/src/assets/gltf-glb/camera face 2.gltf @@ -0,0 +1,121 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v3.6.28", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 0 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"Circle" + } + ], + "materials":[ + { + "doubleSided":true, + "name":"Material", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.011087078601121902, + 0.011087078601121902, + 0.011087078601121902, + 1 + ], + "metallicFactor":0, + "roughnessFactor":0.4000000059604645 + } + } + ], + "meshes":[ + { + "name":"Circle", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":1910, + "max":[ + 0.29999983310699463, + 0.30012279748916626, + 0.6709707379341125 + ], + "min":[ + -0.30000022053718567, + -0.2998772859573364, + -0.3290294408798218 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":1910, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":1910, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":6084, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":22920, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":22920, + "byteOffset":22920, + "target":34962 + }, + { + "buffer":0, + "byteLength":15280, + "byteOffset":45840, + "target":34962 + }, + { + "buffer":0, + "byteLength":12168, + "byteOffset":61120, + "target":34963 + } + ], + "buffers":[ + { + "byteLength":73288, + "uri":"data:application/octet-stream;base64,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WQ08PUrllz6Idqi+qs65PW+skz4Sdai+qs65PW+skz4Sdai+px/xPUeJjj4Zdai+px/xPUeJjj4Zdai+px/xPUeJjj4Zdai+zDcVPoSYgz7DQpG+zDcVPoSYgz7DQpG+HKMvPlDfcj41dX2+HKMvPlDfcj41dX2+uVFEPuzPYj4YE2G+uVFEPuzPYj4YE2G+q1BYPjI1Uz5iZka+q1BYPjI1Uz5iZka+0OphPu+USj7UlTu+0OphPu+USj7UlTu+P5thPkDNNz6LXha9NtZqPqY2LD6NXha9Rc1zPlypHz6SXha95CCBPtvQBz6UXha9sTSHPs/23D2YXha9mCWMPpdgpj2bXha9xpiPPotBXz2dXha99qCRPpsz5jydXha9alqSPhSJDrmeXha9FJqRPmFl6LydXha9pouPPt5KYL2VXha9hxKMPpvYpr2TXha9LhyHPoVe3b2QXha99QOBPhj7B76EXha9Yo1zPpLHH76CXha99pNqPtdNLL5+Xha9V1dhPq7dN757Xha9AjtYPo4uQr55Xha9bqlPPtELS754Xha9Va89PvFvW75yXha9kJwpPnDUar5rXha9NocOPlete75lXha9A8/gPYMHhb5eXha9fAOtPX6kib5WXha98SwwPbhwjr5MXha968iqs9wUkL4/Xha9FS0wvbdwjr4yXha9jQOtvX6kib43Xha9Fc/gvYMHhb4wXha9RIcOvk+te74oXha9nZwpvmnUar4iXha9Xq89vu5vW74cXha9e6lPvssLS74WXha9EztYvoMuQr4WXha9YVdhvq7dN74TXha9/pNqvtRNLL4QXha9a41zvovHH74MXha9+gOBvg/7B74JXha9MhyHvm5e3b0OXha9jBKMvoPYpr0LXha9p4uPvrpKYL0IXha9FZqRvgpl6LwQXha9a1qSvqdrDrkQXha9+KCRvs8z5jwQXha9x5iPvqNBXz0QXha9mSWMvp5gpj0TXha9sTSHvs723D0WXha95SCBvt3QBz4ZXha9Ts1zvl+pHz4cXha9PtZqvqQ2LD4fXha9RpthvkPNNz4iXha99H9YvmwkQj4lXha93e5PvkYHSz4mXha9VPQ9vm52Wz4zXha9ld4pvojmaj4wXha9BcIOvmTNez44Xha9tC7hveYdhT5AXha9m0+tveK+iT5HXha9qn0wvWWPjj5SXha9wG8KMA01kD5fXha9nX0wPWWPjj5rXha9kE+tPeK+iT52Xha9ti7hPeUdhT59Xha9AsIOPmLNez6FXha9kN4pPormaj6OXha9TvQ9Pm52Wz6KXha92+5PPkMHSz6HXha98H9YPmckQj6IXha9QJthPkDNNz43efm8QJthPkDNNz43efm8N9ZqPqY2LD46efm8N9ZqPqY2LD46efm8R81zPlypHz5Aefm8R81zPlypHz5Aefm85SCBPtvQBz5Hefm85SCBPtvQBz5Hefm8sjSHPs/23D1Oefm8sjSHPs/23D1Oefm8mSWMPpdgpj1Uefm8mSWMPpdgpj1Uefm8x5iPPotBXz1Yefm8x5iPPotBXz1Yefm896CRPp0z5jxKefm896CRPp0z5jxKefm8alqSPrKIDrlKefm8alqSPrKIDrlKefm8FZqRPmBl6LxKefm8FZqRPmBl6LxKefm8pouPPt5KYL04efm8pouPPt5KYL04efm8iBKMPpvYpr00efm8iBKMPpvYpr00efm8LxyHPoVe3b0tefm8LxyHPoVe3b0tefm89gOBPhj7B74mefm89gOBPhj7B74mefm8ZI1zPpLHH74hefm8ZI1zPpLHH74hefm895NqPtdNLL4befm895NqPtdNLL4befm8WVdhPq7dN74Vefm8WVdhPq7dN74Vefm8BDtYPo4uQr4Refm8BDtYPo4uQr4Refm8b6lPPtELS74Mefm8b6lPPtELS74Mefm8Va89PvFvW74Befm8Va89PvFvW74Befm8kJwpPnDUar74ePm8kJwpPnDUar74ePm8NocOPlete77oePm8NocOPlete77oePm8Bc/gPYMHhb7XePm8Bc/gPYMHhb7XePm8fgOtPX6kib7MePm8fgOtPX6kib7MePm88SwwPbhwjr61ePm88SwwPbhwjr61ePm8y0+cs9wUkL6bePm8y0+cs9wUkL6bePm8FS0wvbdwjr6CePm8FS0wvbdwjr6CePm8iwOtvX6kib58ePm8iwOtvX6kib58ePm8E8/gvYMHhb5xePm8E8/gvYMHhb5xePm8RIcOvk+te75fePm8RIcOvk+te75fePm8nZwpvmnUar5QePm8nZwpvmnUar5QePm8Xq89vu5vW75VePm8Xq89vu5vW75VePm8ealPvssLS75MePm8ealPvssLS75MePm8ETtYvoMuQr5HePm8ETtYvoMuQr5HePm8YFdhvq7dN75DePm8YFdhvq7dN75DePm8/ZNqvtRNLL48ePm8/ZNqvtRNLL48ePm8aY1zvovHH743ePm8aY1zvovHH743ePm8+QOBvg/7B74wePm8+QOBvg/7B74wePm8MhyHvm5e3b0pePm8MhyHvm5e3b0pePm8ixKMvoPYpr0kePm8ixKMvoPYpr0kePm8pouPvrpKYL0gePm8pouPvrpKYL0gePm8FZqRvgll6LwuePm8FZqRvgll6LwuePm8alqSvkNrDrkuePm8alqSvkNrDrkuePm896CRvtEz5jwuePm896CRvtEz5jwuePm8x5iPvqNBXz0/ePm8x5iPvqNBXz0/ePm8mCWMvp5gpj1CePm8mCWMvp5gpj1CePm8sDSHvs723D1JePm8sDSHvs723D1JePm85CCBvt3QBz5QePm85CCBvt3QBz5QePm8TM1zvl+pHz5XePm8TM1zvl+pHz5XePm8PdZqvqQ2LD5dePm8PdZqvqQ2LD5dePm8RJthvkPNNz5gePm8RJthvkPNNz5gePm8839YvmwkQj5mePm8839YvmwkQj5mePm83O5PvkYHSz5pePm83O5PvkYHSz5pePm8VPQ9vm52Wz51ePm8VPQ9vm52Wz51ePm8ld4pvojmaj6AePm8ld4pvojmaj6AePm8BcIOvmTNez6OePm8BcIOvmTNez6OePm8si7hveYdhT6gePm8si7hveYdhT6gePm8mU+tveK+iT6sePm8mU+tveK+iT6sePm8qn0wvWWPjj7BePm8qn0wvWWPjj7BePm8+t/4MQ01kD7cePm8+t/4MQ01kD7cePm8nX0wPWWPjj70ePm8nX0wPWWPjj70ePm8kk+tPeK+iT77ePm8kk+tPeK+iT77ePm8uC7hPeUdhT4Hefm8uC7hPeUdhT4Hefm8AsIOPmLNez4Xefm8AsIOPmLNez4Xefm8kN4pPormaj4nefm8kN4pPormaj4nefm8TvQ9Pm52Wz4hefm8TvQ9Pm52Wz4hefm83O5PPkMHSz4uefm83O5PPkMHSz4uefm88n9YPmckQj4xefm88n9YPmckQj4xefm8DmNEPksDID50Tki0DmNEPksDID50Tki0DmNEPksDID50Tki0DmNEPksDID50Tki0+mtMPursFT7s/FC0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AACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwAAAAAAAIA/AAAAAAAAgD8AAAAAAACAPwUAAwABAAEAPwA9AD0AOwA5ADkANwA1ADUAMwAxADEALgAsACwAKgAoACgAJgAkACQAIgAgACAAHgAcABwAGgAYABgAFgAUABQAEgAQABAADwANAA0ACwAJAAkABwAFAAUAAQA9AD0AOQA1ADUAMQAsACwAKAAkACQAIAAcABwAGAAUABQAEAANAA0ACQAFAAUAPQA1ADUALAAkACQAHAAUABQADQAFAAUANQAkACQAFAAFAFEAUwCTAFEAkwCRABsAHQBcABsAXABaAAAAAgBBAAAAQQBDADYAOAB5ADYAeQB3AB0AHwBeAB0AXgBcAAIABABFAAIARQBBADgAOgB7ADgAewB5AB8AIQBgAB8AYABeAAQABgBHAAQARwBFADoAPAB9ADoAfQB7ACEAIwBiACEAYgBgAAYACABJAAYASQBHADwAPgB/ADwAfwB9ACMAJQBkACMAZABiAAgACgBLAAgASwBJAD4AAABDAD4AQwB/ACUAJwBmACUAZgBkAAoADABNAAoATQBLACcAKQBoACcAaABmAAwADgBPAAwATwBNACkAKwBqACkAagBoAA4AEQBQAA4AUABPACsALQBsACsAbABqABEAEwBSABEAUgBQAC0ALwBuAC0AbgBsABMAFQBUABMAVABSAC8AMABxAC8AcQBuABUAFwBWABUAVgBUADAAMgBzADAAcwBxABcAGQBYABcAWABWADIANAB1ADIAdQBzABkAGwBaABkAWgBYADQANgB3ADQAdwB1AJYAmADYAJYA2ADWAG0AbwCvAG0ArwCtAFMAVQCVAFMAlQCTAG8AcACwAG8AsACvAFUAVwCXAFUAlwCVAHAAcgCyAHAAsgCwAFcAWQCZAFcAmQCXAHIAdAC0AHIAtACyAFkAWwCbAFkAmwCZAHQAdgC2AHQAtgC0AFsAXQCdAFsAnQCbAEIAQACAAEIAgACCAHYAeAC4AHYAuAC2AF0AXwCfAF0AnwCdAEAARACEAEAAhACAAHgAegC6AHgAugC4AF8AYQChAF8AoQCfAEQARgCGAEQAhgCEAHoAfAC8AHoAvAC6AGEAYwCjAGEAowChAEYASACIAEYAiACGAHwAfgC+AHwAvgC8AGMAZQClAGMApQCjAEgASgCKAEgAigCIAH4AQgCCAH4AggC+AGUAZwCnAGUApwClAEoATACMAEoAjACKAGcAaQCpAGcAqQCnAEwATgCOAEwAjgCMAGkAawCrAGkAqwCpAE4AUQCRAE4AkQCOAGsAbQCtAGsArQCrAN0A3wAfAd0AHwEdAbMAtQD1ALMA9QDzAJgAmgDaAJgA2gDYALUAtwD3ALUA9wD1AJoAnADcAJoA3ADaAIMAgQDBAIMAwQDDALcAuQD5ALcA+QD3AJwAngDeAJwA3gDcAIEAhQDFAIEAxQDBALkAuwD7ALkA+wD5AJ4AoADgAJ4A4ADeAIUAhwDHAIUAxwDFALsAvQD9ALsA/QD7AKAAogDiAKAA4gDgAIcAiQDJAIcAyQDHAL0AvwD/AL0A/wD9AKIApADkAKIA5ADiAIkAiwDLAIkAywDJAL8AgwDDAL8AwwD/AKQApgDmAKQA5gDkAIsAjQDNAIsAzQDLAKYAqADoAKYA6ADmAI0AjwDPAI0AzwDNAKgAqgDqAKgA6gDoAI8AkADQAI8A0ADPAKoArADsAKoA7ADqAJAAkgDSAJAA0gDQAKwArgDuAKwA7gDsAJIAlADUAJIA1ADSAK4AsQDxAK4A8QDuAJQAlgDWAJQA1gDUALEAswDzALEA8wDxACIBJAFkASIBZAFiAcAAxAAEAcAABAEAAfgA+gA6AfgAOgE4Ad8A4QAhAd8AIQEfAcQAxgAGAcQABgEEAfoA/AA8AfoAPAE6AeEA4wAjAeEAIwEhAcYAyAAIAcYACAEGAfwA/gA+AfwAPgE8AeMA5QAlAeMAJQEjAcgAygAKAcgACgEIAf4AwgACAf4AAgE+AeUA5wAnAeUAJwElAcoAzAAMAcoADAEKAecA6QApAecAKQEnAcwAzgAOAcwADgEMAekA6wArAekAKwEpAc4A0QARAc4AEQEOAesA7QAtAesALQErAdEA0wATAdEAEwERAe0A7wAvAe0ALwEtAdMA1QAVAdMAFQETAe8A8AAwAe8AMAEvAdUA1wAXAdUAFwEVAfAA8gAyAfAAMgEwAdcA2QAZAdcAGQEXAfIA9AA0AfIANAEyAdkA2wAbAdkAGwEZAfQA9gA2AfQANgE0AdsA3QAdAdsAHQEbAcIAwAAAAcIAAAECAfYA+AA4AfYAOAE2AVEBUwGJAVEBiQGIAQkBCwFLAQkBSwFJAT8BAwFDAT8BQwF/ASQBJgFmASQBZgFkAQsBDQFNAQsBTQFLASYBKAFoASYBaAFmAQ0BDwFPAQ0BTwFNASgBKgFqASgBagFoAQ8BEAFQAQ8BUAFPASoBLAFsASoBbAFqARABEgFSARABUgFQASwBLgFuASwBbgFsARIBFAFUARIBVAFSAS4BMQFxAS4BcQFuARQBFgFWARQBVgFUATEBMwFzATEBcwFxARYBGAFYARYBWAFWATMBNQF1ATMBdQFzARgBGgFaARgBWgFYATUBNwF3ATUBdwF1ARoBHAFcARoBXAFaAQMBAQFBAQMBQQFDATcBOQF5ATcBeQF3ARwBHgFeARwBXgFcAQEBBQFFAQEBRQFBATkBOwF7ATkBewF5AR4BIAFgAR4BYAFeAQUBBwFHAQUBRwFFATsBPQF9ATsBfQF7ASABIgFiASABYgFgAQcBCQFJAQcBSQFHAT0BPwF/AT0BfwF9AYsBjAGsAYsBrAGrAW0BbwGXAW0BlwGWAVMBVQGKAVMBigGJAW8BcAGYAW8BmAGXAVUBVwGLAVUBiwGKAXABcgGZAXABmQGYAVcBWQGMAVcBjAGLAXIBdAGaAXIBmgGZAVkBWwGNAVkBjQGMAXQBdgGbAXQBmwGaAVsBXQGOAVsBjgGNAUIBQAGAAUIBgAGBAXYBeAGcAXYBnAGbAV0BXwGPAV0BjwGOAUABRAGCAUABggGAAXgBegGdAXgBnQGcAV8BYQGQAV8BkAGPAUQBRgGDAUQBgwGCAXoBfAGeAXoBngGdAWEBYwGRAWEBkQGQAUYBSAGEAUYBhAGDAXwBfgGfAXwBnwGeAWMBZQGSAWMBkgGRAUgBSgGFAUgBhQGEAX4BQgGBAX4BgQGfAWUBZwGTAWUBkwGSAUoBTAGGAUoBhgGFAWcBaQGUAWcBlAGTAUwBTgGHAUwBhwGGAWkBawGVAWkBlQGUAU4BUQGIAU4BiAGHAWsBbQGWAWsBlgGVAZkBmgG6AZkBugG5AYwBjQGtAYwBrQGsAZoBmwG7AZoBuwG6AY0BjgGuAY0BrgGtAYEBgAGgAYEBoAGhAZsBnAG8AZsBvAG7AY4BjwGvAY4BrwGuAYABggGiAYABogGgAZwBnQG9AZwBvQG8AY8BkAGwAY8BsAGvAYIBgwGjAYIBowGiAZ0BngG+AZ0BvgG9AZABkQGxAZABsQGwAYMBhAGkAYMBpAGjAZ4BnwG/AZ4BvwG+AZEBkgGyAZEBsgGxAYQBhQGlAYQBpQGkAZ8BgQGhAZ8BoQG/AZIBkwGzAZIBswGyAYUBhgGmAYUBpgGlAZMBlAG0AZMBtAGzAYYBhwGnAYYBpwGmAZQBlQG1AZQBtQG0AYcBiAGoAYcBqAGnAZUBlgG2AZUBtgG1AYgBiQGpAYgBqQGoAZYBlwG3AZYBtwG2AYkBigGqAYkBqgGpAZcBmAG4AZcBuAG3AYoBiwGrAYoBqwGqAZgBmQG5AZgBuQG4AcUBxwEHAsUBBwIFAqIBowHGAaIBxgHEAaMBpAHIAaMByAHGAYMCgQJMAoMCTAJKAvsB/QE9AvsBPQI7AuAB4gEiAuABIgIgAscByQEJAscBCQIHAv0B/wE/Av0BPwI9AuIB5AEkAuIBJAIiAskBywELAskBCwIJAv8BwwEDAv8BAwI/AuQB5gEmAuQBJgIkAssBzQENAssBDQILAuYB6AEoAuYBKAImAs0BzwEPAs0BDwINAugB6gEqAugBKgIoAs8B0AEQAs8BEAIPAuoB7AEsAuoBLAIqAtAB0gESAtABEgIQAuwB7gEuAuwBLgIsAtIB1AEUAtIBFAISAu4B8QExAu4BMQIuAtQB1gEWAtQBFgIUAvEB8wEzAvEBMwIxAtYB2AEYAtYBGAIWAvMB9QE1AvMBNQIzAtgB2gEaAtgBGgIYAvUB9wE3AvUBNwI1AtoB3AEcAtoBHAIaAsMBwQEBAsMBAQIDAvcB+QE5AvcBOQI3AtwB3gEeAtwBHgIcAsEBxQEFAsEBBQIBAvkB+wE7AvkBOwI5At4B4AEgAt4BIAIeAuQG4QZYAowCigJoAowCaAJmAoECkQJOAoECTgJMAooClgJqAooCagJoApECmgJRApECUQJOApYCngJsApYCbAJqApoCogJTApoCUwJRAp4CpgJuAp4CbgJsAqICqgJVAqICVQJTAqYCrQJwAqYCcAJuAqoCsgJXAqoCVwJVAq0CtQJxAq0CcQJwArICugJZArICWQJXArUCvQJzArUCcwJxAroCwgJbAroCWwJZAr0CxQJ1Ar0CdQJzAsICygJdAsICXQJbAs8CzQJAAs8CQAJCAsUC1QJ3AsUCdwJ1AsoC2gJfAsoCXwJdAs0C3QJEAs0CRAJAAtUC4QJ5AtUCeQJ3AtoC5gJhAtoCYQJfAt0C6QJGAt0CRgJEAuEC7QJ7AuECewJ5AuYC8gJjAuYCYwJhAukC9QJIAukCSAJGAu0C+QJ9Au0CfQJ7AvIC/gJlAvICZQJjAvUCgwJKAvUCSgJIAvkCzwJCAvkCQgJ9Av4CjAJmAv4CZgJlAiUCJwKOAiUCjgL8AqkCoQKhA6kCoQOpAz4CAgLRAj4C0QL3AikDMQOvAykDrwOnAwgCCgKFAggChQLzAp8CpwItBJ8CLQQxBCMCJQL8AiMC/ALwAi8DJwNTBC8DUwRXBDwCPgL3AjwC9wLrAq4CpQKlA64CpQOuAwYCCALzAgYC8wLnAiUDLgOsAyUDrAOjAyECIwLwAiEC8ALkAqMCrAIsBKMCLAQvBDoCPALrAjoC6wLfAiwDIwNRBCwDUQRWBAQCBgLnAgQC5wLbArECqQKpA7ECqQOxAx8CIQLkAh8C5ALYAiEDKQOnAyEDpwOfAzgCOgLfAjgC3wLTAqcCrwIpBKcCKQQtBAACBALbAgAC2wLLAicDHwNPBCcDTwRTBB0CHwLYAh0C2ALIArYCrgKuA7YCrgO2AzYCOALTAjYC0wLDAh0DJQOjAx0DowObAwICAALLAgICywLRAqwCtAIoBKwCKAQsBBsCHQLIAhsCyALAAiMDGwNNBCMDTQRRBDQCNgLDAjQCwwK7ArkCsQKxA7kCsQO5AxkCGwLAAhkCwAK4AhkDIQOfAxkDnwOXAzICNAK7AjICuwKzAq8CtwIlBK8CJQQpBBcCGQK4AhcCuAKwAh8DFwNLBB8DSwRPBDACMgKzAjACswKrAr4CtgK2A74CtgO+AxUCFwKwAhUCsAKoAn0DCwNFBH0DRQT7Ay8CMAKrAi8CqwKkAhUDHQObAxUDmwOTAxMCFQKoAhMCqAKgArQCvAIkBLQCJAQoBC0CLwKkAi0CpAKcAg0DewN9BA0DfQSLAxECEwKgAhECoAKYAhsDEwNJBBsDSQRNBCsCLQKcAisCnAKUAsECuQK5A8ECuQPBAw4CEQKYAg4CmAKPAnoDUANoBHoDaAT4AykCKwKUAikClAKIAhIDGQOXAxIDlwOQAwwCDgKPAgwCjwJ/ArcCvwIhBLcCIQQlBCcCKQKIAicCiAKOAlIDeAN8BFIDfATQAwoCDAJ/AgoCfwKFAhcDEANIBBcDSARLBMYCvgK+A8YCvgPGAwUD/wIBAwUDAQMDA3YDBANCBHYDQgT0Aw4DCAMKAw4DCgMMAwkDFQOTAwkDkwOHA/8CDwMRA/8CEQMBA7wCxAIgBLwCIAQkBAgDFAMWAwgDFgMKAwYDdAN6BAYDegSEAw8DGAMaAw8DGgMRAxMDBwNDBBMDQwRJBBQDHAMeAxQDHgMWA8kCwQLBA8kCwQPJAxgDIAMiAxgDIgMaA3EDfQP7A3ED+wPvAxwDJAMmAxwDJgMeAwIDEgOQAwIDkAOAAyADKAMqAyADKgMiA78CxwIdBL8CHQQhBCQDKwMtAyQDLQMmA3sDbwN3BHsDdwR9BCgDMAMyAygDMgMqAxADAANABBADQARIBCsDMwM1AysDNQMtA84C0AIGBM4CBgTOAzADOAM6AzADOgMyA24DegP4A24D+APsAzMDOwM9AzMDPQM1AwsDCQOHAwsDhwNFBDgDQANCAzgDQgM6A9ICzAIcBNICHATSAzsDQwNFAzsDRQM9A3gDbAN2BHgDdgR8BEADSANKA0ADSgNCAwcDDQOLAwcDiwNDBFEDSwNNA1EDTQNPA9YCxgLGA9YCxgPWA0MDUwNVA0MDVQNFA2oDdgP0A2oD9APoA0gDWANaA0gDWgNKAwQDAgOAAwQDgANCBEsDWwNdA0sDXQNNA8QC1AIaBMQCGgQgBFMDXwNhA1MDYQNVA3QDaAN0BHQDdAR6BFgDZANmA1gDZgNaAwADBgOEAwADhANABFsDZwNpA1sDaQNdA9kCyQLJA9kCyQPZA18DawNtA18DbQNhA2UDcQPvA2UD7wPjA2QDcANyA2QDcgNmA8cC1wIXBMcCFwQdBGcDcwN1A2cDdQNpA28DYwNxBG8DcQR3BGsDdwN5A2sDeQNtA94CzgLOA94CzgPeA3ADfAN+A3ADfgNyA2IDbgPsA2ID7APgA3MDBQMDA3MDAwN1A8wC3AIWBMwCFgQcBHcDUQNPA3cDTwN5A2wDYANwBGwDcAR2BHwDDgMMA3wDDAN+A/wDRgQCBPwDAgT+A/cDZwQFBPcDBQT5A/MDQQQJBPMDCQT1A/AD/AP+A/AD/gPyA+sD9wP5A+sD+QPtA+cD8wP1A+cD9QPpA+QD8APyA+QD8gPmA98D6wPtA98D7QPhA9sD5wPpA9sD6QPdA9gD5APmA9gD5gPaA9MD3wPhA9MD4QPVA8sD2wPdA8sD3QPNA8gD2APaA8gD2gPKA8MD0wPVA8MD1QPFA2cEywPNA2cEzQMFBMADyAPKA8ADygPCA7sDwwPFA7sDxQO9A7gDwAPCA7gDwgO6A7MDuwO9A7MDvQO1A7ADuAO6A7ADugOyA6sDswO1A6sDtQOtA6gDsAOyA6gDsgOqA6QDqwOtA6QDrQOmA6ADqAOqA6ADqgOiA5wDpAOmA5wDpgOeA5gDoAOiA5gDogOaA5QDnAOeA5QDngOWA48DmAOaA48DmgORA4gDlAOWA4gDlgOKA38DjwORA38DkQOBA0YEiAOKA0YEigMCBEEEfwOBA0EEgQMJBIUDPQQ/BIUDPwSDA44DPAREBI4DRASMAz0EOQRHBD0ERwQ/BDwEOARKBDwESgREBDkENgRMBDkETARHBDgENAROBDgETgRKBDYEMgRQBDYEUARMBDQEMARSBDQEUgROBDIELgRUBDIEVARQBDAEKwRVBDAEVQRSBC4EKgRYBC4EWARUBCsEJwRZBCsEWQRVBCoEJgRcBCoEXARYBCcEIwRdBCcEXQRZBCYEIgRgBCYEYARcBCMEHwRhBCMEYQRdBCIEHgRkBCIEZARgBNEDGwRlBNEDZQTPAx8EGQRpBB8EaQRhBB4EGARsBB4EbARkBBsEFQRtBBsEbQRlBBkEEwRvBBkEbwRpBBgEEgRyBBgEcgRsBBUEDwRzBBUEcwRtBBMEDQR1BBMEdQRvBBIEDAR4BBIEeARyBA8EBwR5BA8EeQRzBA0EAwR7BA0EewR1BAwEAAR+BAwEfgR4BAcEhQODAwcEgwN5BAME0QPPAwMEzwN7BAAEjgOMAwAEjAN+BDQDLANWBDQDVgRaBJsCowIvBJsCLwQzBC4DNgO0Ay4DtAOsA6UCnQKdA6UCnQOlAzcDLwNXBDcDVwRbBJcCnwIxBJcCMQQ1BDEDOQO3AzEDtwOvA6ECmQKZA6ECmQOhAzwDNANaBDwDWgReBPsCjQKNA/sCjQP/A5MCmwIzBJMCMwQ3BDYDPgO8AzYDvAO0A4sC/QL9A4sC/QMBBJ0ClQKVA50ClQOdAz8DNwNbBD8DWwRfBPgC0gLSA/gC0gMEBJAClwI1BJACNQQ6BDkDQQO/AzkDvwO3A9AC+gL6A9AC+gMGBJkCkgKSA5kCkgOZA0QDPANeBEQDXgRiBPQChgKGA/QChgMIBIcCkwI3BIcCNwQ7BD4DRgPEAz4DxAO8A4QC9gL2A4QC9gMKBJUCiQKJA5UCiQOVA0cDPwNfBEcDXwRjBO8C+wL/A+8C/wMLBIACkAI6BIACOgQ+BEEDSQPHA0EDxwO/A/0C8QLxA/0C8QP9A5ICggKCA5ICggOSA0wDUgPQA0wD0ANmBOwC+AIEBOwCBAQOBI0ChwI7BI0COwSNA1ADTgPMA1ADzANoBPoC7gLuA/oC7gP6A4kCiwIBBIkCAQSJA1QDRANiBFQDYgRqBOgC9AIIBOgCCAQQBIYCgAI+BIYCPgSGA0YDVgPUA0YD1APEA/YC6gLqA/YC6gP2A4IChAIKBIICCgSCA1cDRwNjBFcDYwRrBOMC7wILBOMCCwQRBEkDWQPXA0kD1wPHA/EC5QLlA/EC5QPxA1wDTANmBFwDZgRuBOAC7AIOBOACDgQUBE4DXgPcA04D3APMA+4C4gLiA+4C4gPuA2ADVANqBGADagRwBNwC6AIQBNwCEAQWBFYDYgPgA1YD4APUA+oC3gLeA+oC3gPqA2MDVwNrBGMDawRxBNcC4wIRBNcCEQQXBFkDZQPjA1kD4wPXA+UC2QLZA+UC2QPlA2gDXANuBGgDbgR0BNQC4AIUBNQCFAQaBF4DagPoA14D6APcA+IC1gLWA+IC1gPiA6QBpQHKAaQBygHIAaUBpgHMAaUBzAHKAaYBpwHOAaYBzgHMAacBqAHRAacB0QHOAagBqQHTAagB0wHRAakBqgHVAakB1QHTAaoBqwHXAaoB1wHVAasBrAHZAasB2QHXAawBrQHbAawB2wHZAa0BrgHdAa0B3QHbAa4BrwHfAa4B3wHdAa8BsAHhAa8B4QHfAbABsQHjAbAB4wHhAbEBsgHlAbEB5QHjAbIBswHnAbIB5wHlAbMBtAHpAbMB6QHnAbQBtQHrAbQB6wHpAbUBtgHtAbUB7QHrAbYBtwHvAbYB7wHtAbcBuAHwAbcB8AHvAbgBuQHyAbgB8gHwAbkBugH0AbkB9AHyAboBuwH2AboB9gH0AbsBvAH4AbsB+AH2AbwBvQH6AbwB+gH4Ab0BvgH8Ab0B/AH6Ab4BvwH+Ab4B/gH8Ab8BoQHCAb8BwgH+AaEBoAHAAaEBwAHCAaABogHEAaABxAHAAesEQAVBBesEQQXuBLYEJgUnBbYEJwW5BIIEDAUNBYIEDQWEBO4EQQVCBe4EQgXwBLkEJwUoBbkEKAW8BIQEDQUOBYQEDgWGBPAEQgVDBfAEQwXyBLwEKAUpBbwEKQW+BIYEDgUPBYYEDwWIBPIEQwVEBfIERAX0BL4EKQUqBb4EKgW/BIgEDwUQBYgEEAWKBPQERAVFBfQERQX2BL8EKgUrBb8EKwXBBIoEEAURBYoEEQWMBPYERQVGBfYERgX5BMEEKwUsBcEELAXDBIwEEQUSBYwEEgWOBPkERgVHBfkERwX7BMMELAUtBcMELQXFBI4EEgUTBY4EEwWQBPsERwVIBfsESAX9BMUELQUuBcUELgXHBJAEEwUUBZAEFAWSBP0ESAVJBf0ESQX/BMcELgUvBccELwXJBJIEFAUVBZIEFQWUBP8ESQVKBf8ESgUBBckELwUwBckEMAXLBJQEFQUWBZQEFgWWBAEFSgVLBQEFSwUDBcsEMAUxBcsEMQXNBJYEFgUXBZYEFwWYBAMFSwVMBQMFTAUGBc0EMQUyBc0EMgXPBJgEFwUYBZgEGAWaBAYFTAVNBQYFTQUIBc8EMgUzBc8EMwXRBJoEGAUZBZoEGQWcBAgFTQVOBQgFTgUKBdEEMwU0BdEENAXTBJwEGQUaBZwEGgWeBAoFTgULBQoFCwWABNMENAU1BdMENQXVBJ4EGgUbBZ4EGwWgBNUENQU2BdUENgXXBKAEGwUcBaAEHAWiBNcENgU3BdcENwXZBKIEHAUdBaIEHQWkBNkENwU4BdkEOAXbBKQEHQUeBaQEHgWmBNsEOAU5BdsEOQXdBKYEHgUfBaYEHwWnBN0EOQU6Bd0EOgXfBKcEHwUgBacEIAWpBN8EOgU7Bd8EOwXhBKkEIAUhBakEIQWsBOEEOwU8BeEEPAXjBKwEIQUiBawEIgWvBOMEPAU9BeMEPQXlBK8EIgUjBa8EIwWxBOUEPQU+BeUEPgXnBLEEIwUkBbEEJAWyBOcEPgU/BecEPwXpBLIEJAUlBbIEJQW0BOkEPwVABekEQAXrBLQEJQUmBbQEJgW2BIAECwUMBYAEDAWCBD8FtwW5BT8FuQVABSUFgwWFBSUFhQUmBQsFUAVSBQsFUgUMBUAFuQW7BUAFuwVBBSYFhQWHBSYFhwUnBQwFUgVUBQwFVAUNBUEFuwW9BUEFvQVCBScFhwWJBScFiQUoBQ0FVAVWBQ0FVgUOBUIFvQW/BUIFvwVDBSgFiQWLBSgFiwUpBQ4FVgVYBQ4FWAUPBUMFvwXBBUMFwQVEBSkFiwWNBSkFjQUqBQ8FWAVaBQ8FWgUQBUQFwQXDBUQFwwVFBSoFjQWPBSoFjwUrBRAFWgVcBRAFXAURBUUFwwXGBUUFxgVGBSsFjwWRBSsFkQUsBREFXAVeBREFXgUSBUYFxgXIBUYFyAVHBSwFkQWTBSwFkwUtBRIFXgVgBRIFYAUTBUcFyAXKBUcFygVIBS0FkwWVBS0FlQUuBRMFYAViBRMFYgUUBUgFygXMBUgFzAVJBS4FlQWXBS4FlwUvBRQFYgVkBRQFZAUVBUkFzAXOBUkFzgVKBS8FlwWZBS8FmQUwBRUFZAVmBRUFZgUWBUoFzgXQBUoF0AVLBTAFmQWbBTAFmwUxBRYFZgVoBRYFaAUXBUsF0AXSBUsF0gVMBTEFmwWdBTEFnQUyBRcFaAVqBRcFagUYBUwF0gXUBUwF1AVNBTIFnQWfBTIFnwUzBRgFagVsBRgFbAUZBU0F1AXWBU0F1gVOBTMFnwWhBTMFoQU0BRkFbAVuBRkFbgUaBU4F1gVQBU4FUAULBTQFoQWjBTQFowU1BRoFbgVwBRoFcAUbBTUFowWlBTUFpQU2BRsFcAVyBRsFcgUcBTYFpQWnBTYFpwU3BRwFcgV0BRwFdAUdBTcFpwWpBTcFqQU4BR0FdAV2BR0FdgUeBTgFqQWrBTgFqwU5BR4FdgV4BR4FeAUfBTkFqwWtBTkFrQU6BR8FeAV6BR8FegUgBToFrQWvBToFrwU7BSAFegV8BSAFfAUhBTsFrwWxBTsFsQU8BSEFfAV+BSEFfgUiBTwFsQWzBTwFswU9BSIFfgWABSIFgAUjBT0FswW1BT0FtQU+BSMFgAWBBSMFgQUkBT4FtQW3BT4FtwU/BSQFgQWDBSQFgwUlBa4FlAaYBq4FmAawBXkFLgYzBnkFMwZ7BbAFmAacBrAFnAayBXsFMwY3BnsFNwZ9BbIFnAagBrIFoAa0BX0FNwY7Bn0FOwZ/BbQFoAakBrQFpAa2BX8FOwY9Bn8FPQaCBbYFpAaoBrYFqAa4BYIFPQZBBoIFQQaEBbgFqAasBrgFrAa6BYQFQQZEBoQFRAaGBU8F2gXeBU8F3gVRBboFrAawBroFsAa8BYYFRAZIBoYFSAaIBVEF3gXiBVEF4gVTBbwFsAa0BrwFtAa+BYgFSAZMBogFTAaKBVMF4gXmBVMF5gVVBb4FtAa4Br4FuAbABYoFTAZQBooFUAaMBVUF5gXqBVUF6gVXBcAFuAa8BsAFvAbCBYwFUAZUBowFVAaOBVcF6gXuBVcF7gVZBcIFvAbABsIFwAbEBY4FVAZYBo4FWAaQBVkF7gXyBVkF8gVbBcQFwAbFBsQFxQbFBZAFWAZcBpAFXAaSBVsF8gX2BVsF9gVdBcUFxQbJBsUFyQbHBZIFXAZgBpIFYAaUBV0F9gX6BV0F+gVfBccFyQbOBscFzgbJBZQFYAZkBpQFZAaWBV8F+gX+BV8F/gVhBckFzgbSBskF0gbLBZYFZAZoBpYFaAaYBWEF/gUCBmEFAgZjBcsF0gbXBssF1wbNBZgFaAZsBpgFbAaaBWMFAgYGBmMFBgZlBc0F1wbbBs0F2wbPBZoFbAZwBpoFcAacBWUFBgYKBmUFCgZnBc8F2wbgBs8F4AbRBZwFcAZ0BpwFdAaeBWcFCgYOBmcFDgZpBdEF4AblBtEF5QbTBZ4FdAZ4Bp4FeAagBWkFDgYRBmkFEQZrBdMF5QbpBtMF6QbVBaAFeAZ8BqAFfAaiBWsFEQYVBmsFFQZtBdUF6QbaBdUF2gVPBaIFfAaABqIFgAakBW0FFQYZBm0FGQZvBaQFgAaEBqQFhAamBW8FGQYdBm8FHQZxBaYFhAaIBqYFiAaoBXEFHQYhBnEFIQZzBagFiAaMBqgFjAaqBXMFIQYmBnMFJgZ1BaoFjAaQBqoFkAasBXUFJgYqBnUFKgZ3BawFkAaUBqwFlAauBXcFKgYuBncFLgZ5BegG5AZYAuEG3AZWAuEGVgJYAt0G2AZXAtgG1AZVAtgGVQJXAtMGzwZUAs8GywZSAs8GUgJUAssGxgZQAssGUAJSAsYGwQZQAsEGvQZPAsEGTwJQAr0GuQZNAr0GTQJPArkGtQZNArUGsQZLArUGSwJNArEGrQZLAq0GqQZJAq0GSQJLAqkGpQZJAqUGoQZJAqEGnQZJAp0GmQZHAp0GRwJJApkGlQZHApUGkQZFApUGRQJHApEGjQZFAo0GiQZBAo0GQQJFAokGhQZBAoUGgQZDAoUGQwJBAoEGfQZDAn0GeQZ+An0GfgJDAnkGdQZ8AnkGfAJ+AnUGcQZ8AnEGbQZ6AnEGegJ8Am0GaQZ6AmkGZQZ4AmkGeAJ6AmUGYQZ4AmEGXQZ4Al0GWQZ4AlkGVQZ2AlkGdgJ4AlUGUQZ2AlEGTQZ0AlEGdAJ2Ak0GSQZ0AkkGRgZyAkkGcgJ0AkYGQgZvAkYGbwJyAkIGPwZvAj8GOgZtAj8GbQJvAjoGNgZrAjoGawJtAjYGMgZrAjIGLwZpAjIGaQJrAi8GKwZnAi8GZwJpAisGJwZnAicGIgZnAiMGHgZmAiMGZgJoAh4GGgZmAhoGFgZmAhYGEgZmAhIGDQZkAhIGZAJmAg0GCQZkAgkGBQZiAgkGYgJkAgUGAQZiAgEG/QVgAgEGYAJiAv0F+QVgAvkF9QVeAvkFXgJgAvUF8QVeAvEF7QVeAu0F6QVcAu0FXAJeAukF5QVcAuUF4QVaAuUFWgJcAuEF3QVaAt0F2QVaAtkF6AZYAtkFWAJaAusF5wVyB+sFcgd0B+wE7QRYB+wEWAdWB7cEugQkB7cEJAchB4EEgwTvBoEE7wbtBu0E7wRaB+0EWgdYB7gEuwQmB7gEJgcjB4MEhQTxBoME8QbvBu8E8QRcB+8EXAdaB7sEvQQoB7sEKAcmB4UEhwTzBoUE8wbxBvME9QRgB/MEYAddB70EwAQqB70EKgcoB4cEiQT1BocE9QbzBvUE9wRiB/UEYgdgB8AEwgQsB8AELAcqB4kEiwT3BokE9wb1BvcE+ARkB/cEZAdiB8IExAQuB8IELgcsB4sEjQT5BosE+Qb3BvgE+gRmB/gEZgdkB8QExgQwB8QEMAcuB40EjwT7Bo0E+wb5BvoE/ARoB/oEaAdmB8YEyAQyB8YEMgcwB48EkQT9Bo8E/Qb7BvwE/gRqB/wEagdoB8gEygQ0B8gENAcyB5EEkwT/BpEE/wb9BgAFAgVtBwAFbQdrB8oEzAQ2B8oENgc0B5MElQQBB5MEAQf/BgIFBAVvBwIFbwdtB8wEzgQ4B8wEOAc2B5UElwQDB5UEAwcBBwQFBQVxBwQFcQdvB84E0AQ6B84EOgc4B5cEmQQFB5cEBQcDBwUFBwVzBwUFcwdxB9AE0gQ8B9AEPAc6B5kEmwQHB5kEBwcFBwcFCQV1BwcFdQdzB9IE1AQ+B9IEPgc8B5sEnQQJB5sECQcHBwkFfwTrBgkF6wZ1B9QE1gRAB9QEQAc+B50EnwQLB50ECwcJB9YE2ARCB9YEQgdAB58EoQQNB58EDQcLB9gE2gREB9gERAdCB6EEowQPB6EEDwcNB9oE3ARGB9oERgdEB6MEpQQRB6MEEQcPB9wE3gRIB9wESAdGB6UEqAQTB6UEEwcRB94E4ARKB94ESgdIB6gEqgQVB6gEFQcTB+AE4gRMB+AETAdKB6oErQQYB6oEGAcVB+IE5AROB+IETgdMB6sErgQZB6sEGQcXB+QE5gRQB+QEUAdOB64EsAQbB64EGwcZB+YE6ARSB+YEUgdQB7AEswQdB7AEHQcbB+gE6gRUB+gEVAdSB7MEtQQfB7MEHwcdB+oE7ARWB+oEVgdUB7UEtwQhB7UEIQcfB38EgQTtBn8E7QbrBucF4wVwB+cFcAdyB+MF3wVuB+MFbgdwB98F2wVsB98FbAduB9sF1wVpB9sFaQdsB9cF5gZnB9cFZwdpB+YG4gZlB+YGZQdnB+IG3gZjB+IGYwdlB94G2QZhB94GYQdjB9kG1QZfB9kGXwdhB9UG0AZeB9UGXgdfB9AGzQZbB9AGWwdeB80GygZZB80GWQdbB8oGxwZXB8oGVwdZB8cGwwZVB8cGVQdXB8MGvwZTB8MGUwdVB78GuwZRB78GUQdTB7sGtwZPB7sGTwdRB7cGswZNB7cGTQdPB7MGrwZLB7MGSwdNB68GqwZJB68GSQdLB6sGpwZHB6sGRwdJB6cGowZFB6cGRQdHB6MGnwZDB6MGQwdFB58GmwZBB58GQQdDB5sGlwY/B5sGPwdBB5cGkwY9B5cGPQc/B5MGjwY7B5MGOwc9B48GiwY5B48GOQc7B4sGhwY3B4sGNwc5B4cGgwY1B4cGNQc3B4MGfwYzB4MGMwc1B38GewYxB38GMQczB3sGdwYvB3sGLwcxB3cGcwYtB3cGLQcvB3MGbwYrB3MGKwctB28GawYpB28GKQcrB2sGZwYnB2sGJwcpB2cGYwYlB2cGJQcnB2MGXwYiB2MGIgclB18GWwYgB18GIAciB1sGVwYeB1sGHgcgB1cGUwYcB1cGHAceB1MGTwYaB1MGGgccB08GSgYWB08GFgcaB0oGRQYUB0oGFAcWB0UGQAYSB0UGEgcUB0AGPAYQB0AGEAcSBzwGOAYOBzwGDgcQBzgGNAYMBzgGDAcOBzQGMAYKBzQGCgcMBzAGLAYIBzAGCAcKBywGKAYGBywGBgcIBygGJAYEBygGBAcGByQGHwYCByQGAgcEBx8GGwYABx8GAAcCBxsGFwb+BhsG/gYABxcGEwb8BhcG/Ab+BhMGDwb6BhMG+gb8Bg8GCwb4Bg8G+Ab6BgsGBwb2BgsG9gb4BgcGAwb0BgcG9Ab2BgMG/wXyBgMG8gb0Bv8F+wXwBv8F8AbyBvsF9wXuBvsF7gbwBvcF8wXsBvcF7AbuBvMF7wXqBvMF6gbsBu8F6wV0B+8FdAfqBuMG5wbYBdgF3AXgBeAF5AXoBegF7AXwBfAF9AX4BfgF/AUABgAGBAYIBggGDAYQBhAGFAYYBhgGHAYgBiAGJQYpBikGLQYxBjEGNQY5BjkGPgZDBkMGRwZLBksGTgZSBlIGVgZaBloGXgZiBmIGZgZqBmoGbgZyBnIGdgZ6BnoGfgaCBoIGhgaKBooGjgaSBpIGlgaaBpoGngaiBqIGpgaqBqoGrgayBrIGtga6BroGvgbCBsIGxAbIBsgGzAbRBtEG1gbaBtoG3wbjBuMG2AXgBeAF6AXwBfAF+AUABgAGCAYQBhAGGAYgBiAGKQYxBjEGOQZDBkMGSwZSBlIGWgZiBmIGagZyBnIGegaCBoIGigaSBpIGmgaiBqIGqgayBrIGugbCBsIGyAbRBtEG2gbjBuMG4AXwBfAFAAYQBhAGIAYxBjEGQwZSBlIGYgZyBnIGggaSBpIGogayBrIGwgbRBtEG4wbwBfAFEAYxBjEGUgZyBnIGkgayBrIG0QbwBfAFMQZyBvAFcgayBg==" + } + ] +} diff --git a/app/src/assets/gltf-glb/crate_box.glb b/app/src/assets/gltf-glb/crate_box.glb new file mode 100644 index 0000000..df4175b Binary files /dev/null and b/app/src/assets/gltf-glb/crate_box.glb differ diff --git a/app/src/assets/gltf-glb/door.glb b/app/src/assets/gltf-glb/door.glb new file mode 100644 index 0000000..f2a73e1 Binary files /dev/null and b/app/src/assets/gltf-glb/door.glb differ diff --git a/app/src/assets/gltf-glb/window.glb b/app/src/assets/gltf-glb/window.glb new file mode 100644 index 0000000..27ea80e Binary files /dev/null and b/app/src/assets/gltf-glb/window.glb differ diff --git a/app/src/assets/hdr/mudroadpuresky2k.hdr b/app/src/assets/hdr/mudroadpuresky2k.hdr new file mode 100644 index 0000000..0a03764 Binary files /dev/null and b/app/src/assets/hdr/mudroadpuresky2k.hdr differ diff --git a/app/src/assets/image/userImage.png b/app/src/assets/image/userImage.png new file mode 100644 index 0000000..51af26c Binary files /dev/null and b/app/src/assets/image/userImage.png differ diff --git a/app/src/assets/textures/floor/concreteFloorWorn001Diff2k.jpg b/app/src/assets/textures/floor/concreteFloorWorn001Diff2k.jpg new file mode 100644 index 0000000..f8ffbd3 Binary files /dev/null and b/app/src/assets/textures/floor/concreteFloorWorn001Diff2k.jpg differ diff --git a/app/src/assets/textures/floor/concreteFloorWorn001NorGl2k.jpg b/app/src/assets/textures/floor/concreteFloorWorn001NorGl2k.jpg new file mode 100644 index 0000000..896b67f Binary files /dev/null and b/app/src/assets/textures/floor/concreteFloorWorn001NorGl2k.jpg differ diff --git a/app/src/assets/textures/hdr/mudroadpuresky2k.hdr b/app/src/assets/textures/hdr/mudroadpuresky2k.hdr new file mode 100644 index 0000000..0a03764 Binary files /dev/null and b/app/src/assets/textures/hdr/mudroadpuresky2k.hdr differ diff --git a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx index a3a0d28..8309024 100644 --- a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx +++ b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx @@ -44,12 +44,13 @@ const AssetProperties: React.FC = () => { return (
+ {/* Name */}
Selected Object
- {}} /> - {}} /> + { }} /> + { }} />
diff --git a/app/src/components/layout/sidebarRight/simulation/Simulations.tsx b/app/src/components/layout/sidebarRight/simulation/Simulations.tsx index 26d345f..2a361ba 100644 --- a/app/src/components/layout/sidebarRight/simulation/Simulations.tsx +++ b/app/src/components/layout/sidebarRight/simulation/Simulations.tsx @@ -87,9 +87,8 @@ const Simulations: React.FC = () => { {productsList.map((action, index) => (
{
- {Value.map((val) => ( - + {Value.map((val, index) => ( + ))}
diff --git a/app/src/components/ui/Tools.tsx b/app/src/components/ui/Tools.tsx index f6389e9..7f66584 100644 --- a/app/src/components/ui/Tools.tsx +++ b/app/src/components/ui/Tools.tsx @@ -17,6 +17,7 @@ import { handleSaveTemplate } from "../../modules/visualization/handleSaveTempla import { usePlayButtonStore } from "../../store/usePlayButtonStore"; import useTemplateStore from "../../store/useTemplateStore"; import { useSelectedZoneStore } from "../../store/useZoneStore"; +import { useAddAction, useDeleteModels, useSelectedWallItem, useToggleView } from "../../store/store"; const Tools: React.FC = () => { const { templates } = useTemplateStore(); @@ -32,12 +33,32 @@ const Tools: React.FC = () => { const { addTemplate } = useTemplateStore(); const { selectedZone } = useSelectedZoneStore(); + // wall options + const { setToggleView } = useToggleView(); + const { setDeleteModels } = useDeleteModels(); + const { setAddAction } = useAddAction(); + const { setSelectedWallItem } = useSelectedWallItem(); + + // Reset activeTool whenever activeModule changes useEffect(() => { setActiveTool(activeSubTool); setActiveSubTool(activeSubTool); }, [activeModule]); + const toggleSwitch = () => { + if (toggleThreeD) { + setSelectedWallItem(null); + setDeleteModels(false); + setAddAction(null); + setToggleView(true); + } + else { + setToggleView(false); + } + setToggleThreeD(!toggleThreeD); + }; + useEffect(() => { const handleOutsideClick = (event: MouseEvent) => { if ( @@ -61,9 +82,8 @@ const Tools: React.FC = () => {
{activeSubTool == "cursor" && (
{ setActiveTool("cursor"); }} @@ -73,9 +93,8 @@ const Tools: React.FC = () => { )} {activeSubTool == "free-hand" && (
{ setActiveTool("free-hand"); }} @@ -134,9 +153,8 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-wall"); }} @@ -144,9 +162,8 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-zone"); }} @@ -154,9 +171,8 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-aisle"); }} @@ -164,9 +180,8 @@ const Tools: React.FC = () => {
{ setActiveTool("draw-floor"); }} @@ -233,9 +248,7 @@ const Tools: React.FC = () => {
{ - setToggleThreeD(!toggleThreeD); - }} + onClick={toggleSwitch} >
2d diff --git a/app/src/modules/builder/csg/csg.tsx b/app/src/modules/builder/csg/csg.tsx new file mode 100644 index 0000000..0926710 --- /dev/null +++ b/app/src/modules/builder/csg/csg.tsx @@ -0,0 +1,54 @@ +import * as THREE from "three"; +import { Geometry, Base, Subtraction } from "@react-three/csg"; +import { useDeleteModels } from "../../../store/store"; +import { useRef } from "react"; + +export interface CsgProps { + position: THREE.Vector3 | [number, number, number]; + scale: THREE.Vector3 | [number, number, number]; + model: THREE.Object3D; + hoveredDeletableWallItem: { current: THREE.Mesh | null }; +} + +export const Csg: React.FC = (props) => { + const { deleteModels } = useDeleteModels(); + const modelRef = useRef(); + const originalMaterials = useRef>(new Map()); + + const handleHover = (hovered: boolean, object: THREE.Mesh | null) => { + if (modelRef.current && deleteModels) { + modelRef.current.traverse((child) => { + if (child instanceof THREE.Mesh) { + if (!originalMaterials.current.has(child)) { + originalMaterials.current.set(child, child.material); + } + child.material = child.material.clone(); + child.material.color.set(hovered && deleteModels ? 0xff0000 : (originalMaterials.current.get(child) as any).color); + } + }); + } + props.hoveredDeletableWallItem.current = hovered ? object : null; + }; + + return ( + + + + + + + { + e.stopPropagation(); + handleHover(true, e.object.parent); + }} + onPointerOut={(e: any) => { + e.stopPropagation(); + handleHover(false, null); + }} + /> + + ); +}; diff --git a/app/src/modules/builder/eventDeclaration/dragControlDeclaration.ts b/app/src/modules/builder/eventDeclaration/dragControlDeclaration.ts new file mode 100644 index 0000000..3e5bc5c --- /dev/null +++ b/app/src/modules/builder/eventDeclaration/dragControlDeclaration.ts @@ -0,0 +1,80 @@ +import * as THREE from 'three'; +import { DragControls } from 'three/examples/jsm/controls/DragControls'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import DragPoint from '../geomentries/points/dragPoint'; + +import * as Types from "../../../types/world/worldTypes"; +// import { updatePoint } from '../../../services/factoryBuilder/lines/updatePointApi'; +import { Socket } from 'socket.io-client'; + +export default async function addDragControl( + dragPointControls: Types.RefDragControl, + currentLayerPoint: Types.RefMeshArray, + state: Types.ThreeState, + floorPlanGroupPoint: Types.RefGroup, + floorPlanGroupLine: Types.RefGroup, + lines: Types.RefLines, + onlyFloorlines: Types.RefOnlyFloorLines, + socket: Socket +) { + + ////////// Dragging Point and also change the size to indicate during hover ////////// + + dragPointControls.current = new DragControls(currentLayerPoint.current, state.camera, state.gl.domElement); + dragPointControls.current.enabled = false; + + dragPointControls.current.addEventListener('drag', function (event) { + const object = event.object; + if (object.visible) { + (state.controls as any).enabled = false; + DragPoint(event as any, floorPlanGroupPoint, floorPlanGroupLine, state.scene, lines, onlyFloorlines) + } else { + (state.controls as any).enabled = true; + } + }); + + dragPointControls.current.addEventListener('dragstart', function (event) { + }); + + dragPointControls.current.addEventListener('dragend', async function (event) { + if (!dragPointControls.current) return; + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // await updatePoint( + // organization, + // { "x": event.object.position.x, "y": 0.01, "z": event.object.position.z }, + // event.object.uuid, + // ) + + //SOCKET + + const data = { + organization: organization, + position: { "x": event.object.position.x, "y": 0.01, "z": event.object.position.z }, + uuid: event.object.uuid, + socketId: socket.id + } + + socket.emit('v1:Line:update', data); + + if (state.controls) { + (state.controls as any).enabled = true; + } + }); + + dragPointControls.current.addEventListener('hoveron', function (event: any) { + if ((event.object as Types.Mesh).name === "point") { + event.object.material.uniforms.uInnerColor.value.set(event.object.userData.color) + } + }); + + dragPointControls.current.addEventListener('hoveroff', function (event: any) { + if ((event.object as Types.Mesh).name === "point") { + event.object.material.uniforms.uInnerColor.value.set(new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor)) + } + }); + +} \ No newline at end of file diff --git a/app/src/modules/builder/eventFunctions/handleContextMenu.ts b/app/src/modules/builder/eventFunctions/handleContextMenu.ts new file mode 100644 index 0000000..fad1e64 --- /dev/null +++ b/app/src/modules/builder/eventFunctions/handleContextMenu.ts @@ -0,0 +1,8 @@ +import * as Types from "../../../types/world/worldTypes"; + +export default function handleContextMenu( + menuVisible: Types.Boolean, + setMenuVisible: Types.BooleanState +): void { + // setMenuVisible(true) +} \ No newline at end of file diff --git a/app/src/modules/builder/eventFunctions/handleMeshDown.ts b/app/src/modules/builder/eventFunctions/handleMeshDown.ts new file mode 100644 index 0000000..43806b8 --- /dev/null +++ b/app/src/modules/builder/eventFunctions/handleMeshDown.ts @@ -0,0 +1,64 @@ +import * as THREE from 'three'; + +import * as Types from "../../../types/world/worldTypes"; + +function handleMeshDown( + event: Types.MeshEvent, + currentWallItem: Types.RefMesh, + setSelectedWallItem: Types.setSelectedWallItemSetState, + setSelectedItemsIndex: Types.setSelectedItemsIndexSetState, + wallItems: Types.wallItems, + toggleView: Types.Boolean +): void { + + ////////// To select which of the Wall item and CSG is selected to be dragged ////////// + + if (!toggleView) { + if (currentWallItem.current) { + currentWallItem.current.children.forEach((child) => { + if ((child as THREE.Mesh).isMesh && child.name !== "CSG_REF") { + const material = (child as THREE.Mesh).material; + if (Array.isArray(material)) { + material.forEach(mat => { + if (mat instanceof THREE.MeshStandardMaterial) { + mat.emissive = new THREE.Color("black"); + } + }); + } else if (material instanceof THREE.MeshStandardMaterial) { + material.emissive = new THREE.Color("black"); + } + } + }); + currentWallItem.current = null; + setSelectedWallItem(null); + setSelectedItemsIndex(null); + } + + if (event.intersections.length > 0) { + const clickedIndex = wallItems.findIndex((item) => item.model === event.intersections[0]?.object?.parent?.parent); + if (clickedIndex !== -1) { + setSelectedItemsIndex(clickedIndex); + const wallItemModel = wallItems[clickedIndex]?.model; + if (wallItemModel && wallItemModel.parent && wallItemModel.parent.parent) { + currentWallItem.current = (wallItemModel.parent.parent.children[0]?.children[1]?.children[0] as Types.Mesh) || null; + setSelectedWallItem(wallItemModel.parent); + // currentWallItem.current?.children.forEach((child) => { + // if ((child as THREE.Mesh).isMesh && child.name !== "CSG_REF") { + // const material = (child as THREE.Mesh).material; + // if (Array.isArray(material)) { + // material.forEach(mat => { + // if (mat instanceof THREE.MeshStandardMaterial) { + // mat.emissive = new THREE.Color("green"); + // } + // }); + // } else if (material instanceof THREE.MeshStandardMaterial) { + // material.emissive = new THREE.Color("green"); + // } + // } + // }); + } + } + } + } +} +export default handleMeshDown; diff --git a/app/src/modules/builder/eventFunctions/handleMeshMissed.ts b/app/src/modules/builder/eventFunctions/handleMeshMissed.ts new file mode 100644 index 0000000..9588a48 --- /dev/null +++ b/app/src/modules/builder/eventFunctions/handleMeshMissed.ts @@ -0,0 +1,34 @@ +import * as THREE from 'three'; +import * as Types from "../../../types/world/worldTypes"; + +function handleMeshMissed( + currentWallItem: Types.RefMesh, + setSelectedWallItem: Types.setSelectedWallItemSetState, + setSelectedItemsIndex: Types.setSelectedItemsIndexSetState +): void { + + ////////// If an item is selected and then clicked outside other than the selected object, this runs and removes the color of the selected object and sets setSelectedWallItem and setSelectedItemsIndex as null ////////// + + if (currentWallItem.current) { + currentWallItem.current.children.forEach((child) => { + if ((child as THREE.Mesh).isMesh && child.name !== "CSG_REF") { + const material = (child as THREE.Mesh).material; + + if (Array.isArray(material)) { + material.forEach(mat => { + if (mat instanceof THREE.MeshStandardMaterial) { + mat.emissive = new THREE.Color("black"); + } + }); + } else if (material instanceof THREE.MeshStandardMaterial) { + material.emissive = new THREE.Color("black"); + } + } + }); + currentWallItem.current = null; + setSelectedWallItem(null); + setSelectedItemsIndex(null); + } +} + +export default handleMeshMissed; diff --git a/app/src/modules/builder/functions/deletableLineOrPoint.ts b/app/src/modules/builder/functions/deletableLineOrPoint.ts new file mode 100644 index 0000000..423c581 --- /dev/null +++ b/app/src/modules/builder/functions/deletableLineOrPoint.ts @@ -0,0 +1,87 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import * as Types from "../../../types/world/worldTypes"; + +function DeletableLineorPoint( + state: Types.ThreeState, + plane: Types.RefMesh, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroupPoint: Types.RefGroup, + hoveredDeletableLine: Types.RefMesh, + hoveredDeletablePoint: Types.RefMesh +): void { + + ////////// Altering the color of the hovered line or point during the deletion time ////////// + + if (!plane.current) return; + let intersects = state.raycaster.intersectObject(plane.current, true); + + let visibleIntersectLines; + if (floorPlanGroupLine.current) { visibleIntersectLines = state.raycaster?.intersectObjects(floorPlanGroupLine.current.children, true); } + const visibleIntersectLine = visibleIntersectLines?.find(intersect => intersect.object.visible) as THREE.Line | undefined || null; + + let visibleIntersectPoints; + if (floorPlanGroupPoint.current) { + visibleIntersectPoints = state.raycaster?.intersectObjects(floorPlanGroupPoint.current.children, true); + } + const visibleIntersectPoint = visibleIntersectPoints?.find(intersect => intersect.object.visible) as THREE.Mesh | undefined; + + function getLineColor(lineType: string | undefined): string { + switch (lineType) { + case CONSTANTS.lineConfig.wallName: return CONSTANTS.lineConfig.wallColor; + case CONSTANTS.lineConfig.floorName: return CONSTANTS.lineConfig.floorColor; + case CONSTANTS.lineConfig.aisleName: return CONSTANTS.lineConfig.aisleColor; + default: return CONSTANTS.lineConfig.defaultColor; + } + } + + if (intersects.length > 0) { + if (visibleIntersectPoint) { + if (hoveredDeletableLine.current) { + const lineType = hoveredDeletableLine.current.userData.linePoints[1]?.[3]; + const color = getLineColor(lineType); + (hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color(color); + hoveredDeletableLine.current = null; + } + + hoveredDeletablePoint.current = (visibleIntersectPoint as any).object; + (hoveredDeletablePoint.current as any).material.uniforms.uInnerColor.value.set(new THREE.Color("red")); + (hoveredDeletablePoint.current as any).material.uniforms.uColor.value.set(new THREE.Color("red")); + // (hoveredDeletablePoint.current as THREE.Mesh).scale.set(1.5, 1.5, 1.5); + } else if (hoveredDeletablePoint.current) { + (hoveredDeletablePoint.current as any).material.uniforms.uInnerColor.value.set(CONSTANTS.pointConfig.defaultInnerColor); + (hoveredDeletablePoint.current as any).material.uniforms.uColor.value.set((hoveredDeletablePoint.current as any).userData.color); + // hoveredDeletablePoint.current.scale.set(1, 1, 1); + hoveredDeletablePoint.current = null; + } + + if (visibleIntersectLine && !visibleIntersectPoint) { + if (hoveredDeletableLine.current) { + const lineType = hoveredDeletableLine.current.userData.linePoints[1]?.[3]; + const color = getLineColor(lineType); + (hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color(color); + hoveredDeletableLine.current = null; + } + + if (hoveredDeletablePoint.current) { + (hoveredDeletablePoint.current as any).material.uniforms.uInnerColor.value.set(CONSTANTS.pointConfig.defaultInnerColor); + (hoveredDeletablePoint.current as any).material.uniforms.uColor.value.set((hoveredDeletablePoint.current as any).userData.color); + // hoveredDeletablePoint.current.scale.set(1, 1, 1); + hoveredDeletablePoint.current = null; + } + + hoveredDeletableLine.current = (visibleIntersectLine as any).object; + if (hoveredDeletableLine.current) { + (hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color("red"); + } + } else if (hoveredDeletableLine.current) { + const lineType = hoveredDeletableLine.current.userData.linePoints[1]?.[3]; + const color = getLineColor(lineType); + (hoveredDeletableLine.current.material as THREE.MeshBasicMaterial).color = new THREE.Color(color); + hoveredDeletableLine.current = null; + } + } + +} + +export default DeletableLineorPoint; diff --git a/app/src/modules/builder/functions/draw.ts b/app/src/modules/builder/functions/draw.ts new file mode 100644 index 0000000..341c741 --- /dev/null +++ b/app/src/modules/builder/functions/draw.ts @@ -0,0 +1,97 @@ +import * as Types from "../../../types/world/worldTypes"; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import createAndMoveReferenceLine from "../geomentries/lines/createAndMoveReferenceLine"; + +async function Draw( + state: Types.ThreeState, + plane: Types.RefMesh, + cursorPosition: Types.Vector3, + floorPlanGroupPoint: Types.RefGroup, + floorPlanGroupLine: Types.RefGroup, + snappedPoint: Types.RefVector3, + isSnapped: Types.RefBoolean, + isSnappedUUID: Types.RefString, + line: Types.RefLine, + lines: Types.RefLines, + ispreSnapped: Types.RefBoolean, + floorPlanGroup: Types.RefGroup, + ReferenceLineMesh: Types.RefMesh, + LineCreated: Types.RefBoolean, + setRefTextUpdate: Types.NumberIncrementState, + Tube: Types.RefTubeGeometry, + anglesnappedPoint: Types.RefVector3, + isAngleSnapped: Types.RefBoolean, + toolMode: Types.String, +): Promise { + + ////////// Snapping the cursor during the drawing time and also changing the color of the intersected lines ////////// + + if (!plane.current) return; + const intersects = state.raycaster.intersectObject(plane.current, true); + + if (intersects.length > 0 && (toolMode === "Wall" || toolMode === "Aisle" || toolMode === "Floor")) { + const intersectionPoint = intersects[0].point; + cursorPosition.copy(intersectionPoint); + const snapThreshold = 1; + + if (line.current.length === 0) { + for (const point of floorPlanGroupPoint.current.children) { + const pointType = point.userData.type; + + const canSnap = + ((toolMode === "Wall") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) || + ((toolMode === "Floor") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) || + ((toolMode === "Aisle") && pointType === CONSTANTS.lineConfig.aisleName);; + + if (canSnap && cursorPosition.distanceTo(point.position) < snapThreshold + 0.5 && point.visible) { + cursorPosition.copy(point.position); + snappedPoint.current = point.position; + ispreSnapped.current = true; + isSnapped.current = false; + isSnappedUUID.current = point.uuid; + break; + } else { + ispreSnapped.current = false; + } + } + } else if (line.current.length > 0 && line.current[0]) { + for (const point of floorPlanGroupPoint.current.children) { + const pointType = point.userData.type; + + let canSnap = + ((toolMode === "Wall") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) || + ((toolMode === "Floor") && (pointType === CONSTANTS.lineConfig.wallName || pointType === CONSTANTS.lineConfig.floorName)) || + ((toolMode === "Aisle") && pointType === CONSTANTS.lineConfig.aisleName); + + if (canSnap && cursorPosition.distanceTo(point.position) < snapThreshold && point.visible) { + cursorPosition.copy(point.position); + snappedPoint.current = point.position; + isSnapped.current = true; + ispreSnapped.current = false; + isSnappedUUID.current = point.uuid; + break; + } else { + isSnapped.current = false; + } + } + + createAndMoveReferenceLine( + line.current[0][0], + cursorPosition, + isSnapped, + ispreSnapped, + line, + setRefTextUpdate, + floorPlanGroup, + ReferenceLineMesh, + LineCreated, + Tube, + anglesnappedPoint, + isAngleSnapped + ); + } + } + +} + +export default Draw; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/aisles/addAilseToScene.ts b/app/src/modules/builder/geomentries/aisles/addAilseToScene.ts new file mode 100644 index 0000000..9cd74ed --- /dev/null +++ b/app/src/modules/builder/geomentries/aisles/addAilseToScene.ts @@ -0,0 +1,56 @@ +import * as THREE from 'three'; +import * as Types from '../../../../types/world/worldTypes'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +export default async function addAisleToScene( + aisle: Types.Line, + floorGroupAisle: Types.RefGroup, +): Promise { + if (aisle.length >= 2 && aisle[0] && aisle[1]) { + const start: Types.Vector3 = aisle[0][0]; + const end: Types.Vector3 = aisle[1][0]; + + const direction = new THREE.Vector3( + end.x - start.x, + end.y - start.y, + end.z - start.z + ).normalize(); + + const perp = new THREE.Vector3(-direction.z, 0, direction.x).normalize(); + const offsetDistance = CONSTANTS.aisleConfig.width; + + const leftStart = new THREE.Vector3().copy(start).addScaledVector(perp, offsetDistance); + const rightStart = new THREE.Vector3().copy(start).addScaledVector(perp, -offsetDistance); + const leftEnd = new THREE.Vector3().copy(end).addScaledVector(perp, offsetDistance); + const rightEnd = new THREE.Vector3().copy(end).addScaledVector(perp, -offsetDistance); + + const stripShape = new THREE.Shape(); + stripShape.moveTo(leftStart.x, leftStart.z); + stripShape.lineTo(leftEnd.x, leftEnd.z); + stripShape.lineTo(rightEnd.x, rightEnd.z); + stripShape.lineTo(rightStart.x, rightStart.z); + stripShape.lineTo(leftStart.x, leftStart.z); + + const extrudeSettings = { + depth: CONSTANTS.aisleConfig.height, + bevelEnabled: false, + }; + + const stripGeometry = new THREE.ExtrudeGeometry(stripShape, extrudeSettings); + const stripMaterial = new THREE.MeshStandardMaterial({ + color: CONSTANTS.aisleConfig.defaultColor, + polygonOffset: true, + polygonOffsetFactor: -1, + polygonOffsetUnits: -1, + }); + + const stripMesh = new THREE.Mesh(stripGeometry, stripMaterial); + stripMesh.receiveShadow = true; + stripMesh.castShadow = true; + + stripMesh.position.y = (aisle[0][2] - 1) * CONSTANTS.wallConfig.height + 0.01; + stripMesh.rotateX(Math.PI / 2); + + floorGroupAisle.current.add(stripMesh); + } +} diff --git a/app/src/modules/builder/geomentries/aisles/loadAisles.ts b/app/src/modules/builder/geomentries/aisles/loadAisles.ts new file mode 100644 index 0000000..cceacb8 --- /dev/null +++ b/app/src/modules/builder/geomentries/aisles/loadAisles.ts @@ -0,0 +1,19 @@ +import * as Types from '../../../../types/world/worldTypes'; +import addAisleToScene from './addAilseToScene'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +export default async function loadAisles( + lines: Types.RefLines, + floorGroupAisle: Types.RefGroup +) { + // console.log('lines: ', lines.current[0][0][0]); + if (!floorGroupAisle.current) return + floorGroupAisle.current.children = []; + const aisles = lines.current.filter((line) => line[0][3] && line[1][3] === CONSTANTS.lineConfig.aisleName); + + if (aisles.length > 0) { + aisles.forEach((aisle: Types.Line) => { + addAisleToScene(aisle, floorGroupAisle) + }) + } +} \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/assets/addAssetModel.ts b/app/src/modules/builder/geomentries/assets/addAssetModel.ts new file mode 100644 index 0000000..f211d38 --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/addAssetModel.ts @@ -0,0 +1,186 @@ +import * as THREE from 'three'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import gsap from 'gsap'; +import { toast } from 'react-toastify'; +import TempLoader from './tempLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import * as Types from "../../../../types/world/worldTypes"; +import { retrieveGLTF, storeGLTF } from '../../../../utils/indexDB/idbUtils'; +// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; +import { Socket } from 'socket.io-client'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +async function addAssetModel( + raycaster: THREE.Raycaster, + camera: THREE.Camera, + pointer: THREE.Vector2, + floorGroup: Types.RefGroup, + setFloorItems: Types.setFloorItemSetState, + itemsGroup: Types.RefGroup, + isTempLoader: Types.RefBoolean, + tempLoader: Types.RefMesh, + socket: Socket, + selectedItem: any, + setSelectedItem: any, + plane: Types.RefMesh, +): Promise { + + ////////// Load Floor GLtf's and set the positions, rotation, type etc. in state and store in localstorage ////////// + + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + + try { + isTempLoader.current = true; + 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); + + raycaster.setFromCamera(pointer, camera); + const floorIntersections = raycaster.intersectObjects(floorGroup.current.children, true); + const intersectedFloor = floorIntersections.find(intersect => intersect.object.name.includes("Floor")); + + const planeIntersections = raycaster.intersectObject(plane.current!, true); + const intersectedPlane = planeIntersections[0]; + + let intersectPoint: THREE.Vector3 | null = null; + + if (intersectedFloor && intersectedPlane) { + intersectPoint = intersectedFloor.distance < intersectedPlane.distance ? (new THREE.Vector3(intersectedFloor.point.x, Math.round(intersectedFloor.point.y), intersectedFloor.point.z)) : (new THREE.Vector3(intersectedPlane.point.x, 0, intersectedPlane.point.z)); + } else if (intersectedFloor) { + intersectPoint = new THREE.Vector3(intersectedFloor.point.x, Math.round(intersectedFloor.point.y), intersectedFloor.point.z); + } else if (intersectedPlane) { + intersectPoint = new THREE.Vector3(intersectedPlane.point.x, 0, intersectedPlane.point.z); + } + + if (intersectPoint) { + if (intersectPoint.y < 0) { + intersectPoint = new THREE.Vector3(intersectPoint.x, 0, intersectPoint.z); + } + const cachedModel = THREE.Cache.get(selectedItem.id); + if (cachedModel) { + // console.log(`[Cache] Fetching ${selectedItem.name}`); + handleModelLoad(cachedModel, intersectPoint!, selectedItem, itemsGroup, tempLoader, isTempLoader, setFloorItems, socket); + return; + } else { + const cachedModelBlob = await retrieveGLTF(selectedItem.id); + + if (cachedModelBlob) { + // console.log(`Added ${selectedItem.name} from indexDB`); + + const blobUrl = URL.createObjectURL(cachedModelBlob); + loader.load(blobUrl, (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(selectedItem.id, gltf); + handleModelLoad(gltf, intersectPoint!, selectedItem, itemsGroup, tempLoader, isTempLoader, setFloorItems, socket); + }, + () => { + TempLoader(intersectPoint!, isTempLoader, tempLoader, itemsGroup); + }); + } else { + // console.log(`Added ${selectedItem.name} from Backend`); + + loader.load(`${url_Backend_dwinzo}/api/v1/AssetFile/${selectedItem.id}`, async (gltf) => { + const modelBlob = await fetch(`${url_Backend_dwinzo}/api/v1/AssetFile/${selectedItem.id}`).then((res) => res.blob()); + await storeGLTF(selectedItem.id, modelBlob); + THREE.Cache.add(selectedItem.id, gltf); + await handleModelLoad(gltf, intersectPoint!, selectedItem, itemsGroup, tempLoader, isTempLoader, setFloorItems, socket); + }, + () => { + TempLoader(intersectPoint!, isTempLoader, tempLoader, itemsGroup); + }); + } + } + } + } catch (error) { + console.error('Error fetching asset model:', error); + } finally { + setSelectedItem({}); + } +} + +async function handleModelLoad( + gltf: any, + intersectPoint: THREE.Vector3, + selectedItem: any, + itemsGroup: Types.RefGroup, + tempLoader: Types.RefMesh, + isTempLoader: Types.RefBoolean, + setFloorItems: Types.setFloorItemSetState, + socket: Socket +) { + const model = gltf.scene.clone(); + model.userData = { name: selectedItem.name, modelId: selectedItem.id }; + model.position.set(intersectPoint!.x, 3 + intersectPoint!.y, intersectPoint!.z); + model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); + + model.traverse((child: any) => { + if (child) { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + itemsGroup.current.add(model); + if (tempLoader.current) { + (tempLoader.current.material).dispose(); + (tempLoader.current.geometry).dispose(); + itemsGroup.current.remove(tempLoader.current); + tempLoader.current = undefined; + } + + const newFloorItem: Types.FloorItemType = { + modeluuid: model.uuid, + modelname: selectedItem.name, + modelfileID: selectedItem.id, + position: [intersectPoint!.x, intersectPoint!.y, intersectPoint!.z], + rotation: { x: model.rotation.x, y: model.rotation.y, z: model.rotation.z, }, + isLocked: false, + isVisible: true + }; + + setFloorItems((prevItems) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : "default"; + + //REST + + // await setFloorItemApi( + // organization, + // newFloorItem.modeluuid, + // newFloorItem.modelname, + // newFloorItem.position, + // { "x": model.rotation.x, "y": model.rotation.y, "z": model.rotation.z }, + // newFloorItem.modelfileID!, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modeluuid: newFloorItem.modeluuid, + modelname: newFloorItem.modelname, + modelfileID: newFloorItem.modelfileID, + position: newFloorItem.position, + rotation: { x: model.rotation.x, y: model.rotation.y, z: model.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + }; + + socket.emit("v1:FloorItems:set", data); + + gsap.to(model.position, { y: newFloorItem.position[1], duration: 1.5, ease: "power2.out" }); + gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: "power2.out", onComplete: () => { toast.success("Model Added!"); } }); +} + +export default addAssetModel; diff --git a/app/src/modules/builder/geomentries/assets/assetManager.ts b/app/src/modules/builder/geomentries/assets/assetManager.ts new file mode 100644 index 0000000..837c4d3 --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/assetManager.ts @@ -0,0 +1,153 @@ +import * as THREE from "three"; +import gsap from "gsap"; +import * as Types from "../../../../types/world/worldTypes"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { initializeDB, retrieveGLTF, storeGLTF } from "../../../../utils/indexDB/idbUtils"; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import { toast } from 'react-toastify'; + +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; +let currentTaskId = 0; // Track the active task +let activePromises = new Map(); // Map to track task progress + +export default async function assetManager( + data: any, + itemsGroup: Types.RefGroup, + loader: GLTFLoader, +) { + const taskId = ++currentTaskId; // Increment taskId for each call + activePromises.set(taskId, true); // Mark task as active + + // console.log("Received message from worker:", data); + + if (data.toRemove.length > 0) { + data.toRemove.forEach((uuid: string) => { + const item = itemsGroup.current.getObjectByProperty("uuid", uuid); + if (item) { + // Traverse and dispose of resources + // item.traverse((child: THREE.Object3D) => { + // if (child instanceof THREE.Mesh) { + // if (child.geometry) child.geometry.dispose(); + // if (Array.isArray(child.material)) { + // child.material.forEach((material) => { + // if (material.map) material.map.dispose(); + // material.dispose(); + // }); + // } else if (child.material) { + // if (child.material.map) child.material.map.dispose(); + // child.material.dispose(); + // } + // } + // }); + + // Remove the object from the scene + itemsGroup.current.remove(item); + } + }); + } + + if (data.toAdd.length > 0) { + await initializeDB(); + + for (const item of data.toAdd) { + if (!activePromises.get(taskId)) return; // Stop processing if task is canceled + + await new Promise(async (resolve) => { + const modelUrl = `${url_Backend_dwinzo}/api/v1/AssetFile/${item.modelfileID!}`; + + // Check Three.js Cache + const cachedModel = THREE.Cache.get(item.modelfileID!); + if (cachedModel) { + // console.log(`[Cache] Fetching ${item.modelname}`); + processLoadedModel(cachedModel.scene.clone(), item, itemsGroup, resolve); + return; + } + + // Check IndexedDB + const indexedDBModel = await retrieveGLTF(item.modelfileID!); + if (indexedDBModel) { + // console.log(`[IndexedDB] Fetching ${item.modelname}`); + const blobUrl = URL.createObjectURL(indexedDBModel); + loader.load( + blobUrl, + (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(item.modelfileID!, gltf); // Add to cache + processLoadedModel(gltf.scene.clone(), item, itemsGroup, resolve); + }, + undefined, + (error) => { + toast.error(`[IndexedDB] Error loading ${item.modelname}:`); + resolve(); + } + ); + return; + } + + // Fetch from Backend + // console.log(`[Backend] Fetching ${item.modelname}`); + loader.load( + modelUrl, + async (gltf) => { + const modelBlob = await fetch(modelUrl).then((res) => res.blob()); + await storeGLTF(item.modelfileID!, modelBlob); // Store in IndexedDB + THREE.Cache.add(item.modelfileID!, gltf); // Add to cache + processLoadedModel(gltf.scene.clone(), item, itemsGroup, resolve); + }, + undefined, + (error) => { + toast.error(`[Backend] Error loading ${item.modelname}:`); + resolve(); + } + ); + }); + } + + function processLoadedModel( + gltf: any, + item: Types.FloorItemType, + itemsGroup: Types.RefGroup, + resolve: () => void + ) { + if (!activePromises.get(taskId)) return; // Stop processing if task is canceled + + const existingModel = itemsGroup.current.getObjectByProperty("uuid", item.modeluuid); + if (existingModel) { + // console.log(`Model ${item.modelname} already exists in the scene.`); + resolve(); + return; + } + + const model = gltf; + model.uuid = item.modeluuid; + model.userData = { name: item.modelname, modelId: item.modelfileID }; + model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); + model.position.set(...item.position); + model.rotation.set(item.rotation.x, item.rotation.y, item.rotation.z); + + model.traverse((child: any) => { + if (child.isMesh) { + // Clone the material to ensure changes are independent + // child.material = child.material.clone(); + + child.castShadow = true; + child.receiveShadow = true; + } + }); + + + itemsGroup?.current?.add(model); + + gsap.to(model.position, { y: item.position[1], duration: 1.5, ease: "power2.out" }); + gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 0.5, ease: "power2.out", onStart: resolve, }); + } + } + + activePromises.delete(taskId); // Mark task as complete +} + +// Cancel ongoing task when new call arrives +export function cancelOngoingTasks() { + activePromises.clear(); // Clear all ongoing tasks +} diff --git a/app/src/modules/builder/geomentries/assets/assetVisibility.ts b/app/src/modules/builder/geomentries/assets/assetVisibility.ts new file mode 100644 index 0000000..e2778f6 --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/assetVisibility.ts @@ -0,0 +1,25 @@ +import * as Types from "../../../../types/world/worldTypes"; + +let lastUpdateTime = 0; + +export default function assetVisibility( + itemsGroup: Types.RefGroup, + cameraPosition: Types.Vector3, + renderDistance: Types.Number, + throttleTime = 100 +): void { + const now = performance.now(); + if (now - lastUpdateTime < throttleTime) return; + lastUpdateTime = now; + + if (!itemsGroup?.current || !cameraPosition) return; + + itemsGroup.current.children.forEach((child) => { + const Distance = cameraPosition.distanceTo(child.position); + if (Distance <= renderDistance) { + child.visible = true; + } else { + child.visible = false; + } + }); +} diff --git a/app/src/modules/builder/geomentries/assets/deletableHoveredFloorItems.ts b/app/src/modules/builder/geomentries/assets/deletableHoveredFloorItems.ts new file mode 100644 index 0000000..9af432e --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/deletableHoveredFloorItems.ts @@ -0,0 +1,43 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function DeletableHoveredFloorItems( + state: Types.ThreeState, + itemsGroup: Types.RefGroup, + hoveredDeletableFloorItem: Types.RefMesh, + setDeletableFloorItem: any +): void { + + ////////// Altering the color of the hovered GLTF item during the Deletion time ////////// + + state.raycaster.setFromCamera(state.pointer, state.camera); + const intersects = state.raycaster.intersectObjects(itemsGroup.current.children, true); + + if (intersects.length > 0) { + if (intersects[0].object.name === "Pole") { + return; + } + if (hoveredDeletableFloorItem.current) { + hoveredDeletableFloorItem.current = undefined; + setDeletableFloorItem(null); + } + let currentObject = intersects[0].object; + + while (currentObject) { + if (currentObject.name === "Scene") { + hoveredDeletableFloorItem.current = currentObject as THREE.Mesh; + setDeletableFloorItem(currentObject); + break; + } + currentObject = currentObject.parent as THREE.Object3D; + } + } else { + if (hoveredDeletableFloorItem.current) { + hoveredDeletableFloorItem.current = undefined; + setDeletableFloorItem(null); + } + } +} + +export default DeletableHoveredFloorItems; diff --git a/app/src/modules/builder/geomentries/assets/deleteFloorItems.ts b/app/src/modules/builder/geomentries/assets/deleteFloorItems.ts new file mode 100644 index 0000000..65d5493 --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/deleteFloorItems.ts @@ -0,0 +1,82 @@ +import { toast } from 'react-toastify'; +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; +import { getFloorItems } from '../../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi'; +// import { deleteFloorItem } from '../../../../services/factoryBuilder/assest/floorAsset/deleteFloorItemApi'; +import { Socket } from 'socket.io-client'; + +async function DeleteFloorItems( + itemsGroup: Types.RefGroup, + hoveredDeletableFloorItem: Types.RefMesh, + setFloorItems: Types.setFloorItemSetState, + socket: Socket +): Promise { + + ////////// Deleting the hovered Floor GLTF from the scene (itemsGroup.current) and from the floorItems and also update it in the localstorage ////////// + + if (hoveredDeletableFloorItem.current) { + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const items = await getFloorItems(organization); + const removedItem = items.find( + (item: { modeluuid: string }) => item.modeluuid === hoveredDeletableFloorItem.current?.uuid + ); + + if (!removedItem) { + return + } + + //REST + + // const response = await deleteFloorItem(organization, removedItem.modeluuid, removedItem.modelname); + + //SOCKET + + const data = { + organization: organization, + modeluuid: removedItem.modeluuid, + modelname: removedItem.modelname, + socketId: socket.id + } + + const response = socket.emit('v1:FloorItems:delete', data) + + if (response) { + const updatedItems = items.filter( + (item: { modeluuid: string }) => item.modeluuid !== hoveredDeletableFloorItem.current?.uuid + ); + + const storedItems = JSON.parse(localStorage.getItem("FloorItems") || '[]'); + const updatedStoredItems = storedItems.filter((item: { modeluuid: string }) => item.modeluuid !== hoveredDeletableFloorItem.current?.uuid); + localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); + + if (hoveredDeletableFloorItem.current) { + // Traverse and dispose of resources + hoveredDeletableFloorItem.current.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh) { + if (child.geometry) child.geometry.dispose(); + if (Array.isArray(child.material)) { + child.material.forEach((material) => { + if (material.map) material.map.dispose(); + material.dispose(); + }); + } else if (child.material) { + if (child.material.map) child.material.map.dispose(); + child.material.dispose(); + } + } + }); + + // Remove the object from the scene + itemsGroup.current.remove(hoveredDeletableFloorItem.current); + } + setFloorItems(updatedItems); + toast.success("Model Removed!"); + } + } +} + +export default DeleteFloorItems; diff --git a/app/src/modules/builder/geomentries/assets/tempLoader.ts b/app/src/modules/builder/geomentries/assets/tempLoader.ts new file mode 100644 index 0000000..601b60a --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/tempLoader.ts @@ -0,0 +1,29 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function TempLoader( + intersectPoint: Types.Vector3, + isTempLoader: Types.RefBoolean, + tempLoader: Types.RefMesh, + itemsGroup: Types.RefGroup +): void { + + ////////// Temporary Loader that indicates the gltf is being loaded ////////// + + ////////// Bug: Can't Load More than one TempLoader if done, it won't leave the scene ////////// + + if (tempLoader.current) { + itemsGroup.current.remove(tempLoader.current); + } + if (isTempLoader.current) { + const cubeGeometry = new THREE.BoxGeometry(1, 1, 1); + const cubeMaterial = new THREE.MeshBasicMaterial({ color: "white" }); + tempLoader.current = new THREE.Mesh(cubeGeometry, cubeMaterial); + tempLoader.current.position.set(intersectPoint.x, 0.5 + intersectPoint.y, intersectPoint.z); + itemsGroup.current.add(tempLoader.current); + isTempLoader.current = false; + } +} + +export default TempLoader; diff --git a/app/src/modules/builder/geomentries/floors/addFloorToScene.ts b/app/src/modules/builder/geomentries/floors/addFloorToScene.ts new file mode 100644 index 0000000..acf738b --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/addFloorToScene.ts @@ -0,0 +1,64 @@ +import * as THREE from 'three'; +import * as Types from "../../../../types/world/worldTypes"; +import * as CONSTANTS from "../../../../types/world/worldConstants"; + +import texturePath from "../../../../assets/textures/floor/concreteFloorWorn001Diff2k.jpg"; +import normalPath from "../../../../assets/textures/floor/concreteFloorWorn001NorGl2k.jpg"; + +// Cache for materials +const materialCache = new Map(); + +export default function addFloorToScene( + shape: THREE.Shape, + layer: number, + floorGroup: Types.RefGroup, + userData: any, +) { + const textureLoader = new THREE.TextureLoader(); + + const textureScale = CONSTANTS.floorConfig.textureScale; + + const materialKey = `floorMaterial_${textureScale}`; + + let material: THREE.Material; + + if (materialCache.has(materialKey)) { + material = materialCache.get(materialKey) as THREE.Material; + } else { + const floorTexture = textureLoader.load(texturePath); + const normalMap = textureLoader.load(normalPath); + + floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping; + floorTexture.repeat.set(textureScale, textureScale); + floorTexture.colorSpace = THREE.SRGBColorSpace; + + normalMap.wrapS = normalMap.wrapT = THREE.RepeatWrapping; + normalMap.repeat.set(textureScale, textureScale); + + material = new THREE.MeshStandardMaterial({ + map: floorTexture, + normalMap: normalMap, + side: THREE.DoubleSide, + }); + + materialCache.set(materialKey, material); + } + + const extrudeSettings = { + depth: CONSTANTS.floorConfig.height, + bevelEnabled: false, + }; + + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const mesh = new THREE.Mesh(geometry, material); + + mesh.receiveShadow = true; + mesh.position.y = layer; + mesh.rotateX(Math.PI / 2); + mesh.name = `Floor_Layer_${layer}`; + + // Store UUIDs for debugging or future processing + mesh.userData.uuids = userData; + + floorGroup.current.add(mesh); +} diff --git a/app/src/modules/builder/geomentries/floors/drawOnlyFloor.ts b/app/src/modules/builder/geomentries/floors/drawOnlyFloor.ts new file mode 100644 index 0000000..90f291e --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/drawOnlyFloor.ts @@ -0,0 +1,179 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +import addPointToScene from '../points/addPointToScene'; +import addLineToScene from '../lines/addLineToScene'; +import splitLine from '../lines/splitLine'; +import removeReferenceLine from '../lines/removeReferenceLine'; +import getClosestIntersection from '../lines/getClosestIntersection'; +import arrayLineToObject from '../lines/lineConvertions/arrayLineToObject'; +// import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi'; +import { Socket } from 'socket.io-client'; + +async function drawOnlyFloor( + raycaster: THREE.Raycaster, + state: Types.ThreeState, + camera: THREE.Camera, + plane: Types.RefMesh, + floorPlanGroupPoint: Types.RefGroup, + snappedPoint: Types.RefVector3, + isSnapped: Types.RefBoolean, + isSnappedUUID: Types.RefString, + line: Types.RefLine, + ispreSnapped: Types.RefBoolean, + anglesnappedPoint: Types.RefVector3, + isAngleSnapped: Types.RefBoolean, + onlyFloorline: Types.RefOnlyFloorLine, + onlyFloorlines: Types.RefOnlyFloorLines, + lines: Types.RefLines, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroup: Types.RefGroup, + ReferenceLineMesh: Types.RefMesh, + LineCreated: Types.RefBoolean, + currentLayerPoint: Types.RefMeshArray, + dragPointControls: Types.RefDragControl, + setNewLines: any, + setDeletedLines: any, + activeLayer: Types.Number, + socket: Socket +): Promise { + + ////////// Creating lines Based on the positions clicked ////////// + + if (!plane.current) return + const intersects = raycaster.intersectObject(plane.current, true); + const intersectsLines = raycaster.intersectObjects(floorPlanGroupLine.current.children, true); + const intersectsPoint = raycaster.intersectObjects(floorPlanGroupPoint.current.children, true); + const VisibleintersectsPoint = intersectsPoint.find(intersect => intersect.object.visible); + const visibleIntersect = intersectsLines.find(intersect => intersect.object.visible && intersect.object.name !== CONSTANTS.lineConfig.referenceName); + + if ((intersectsPoint.length === 0 || VisibleintersectsPoint === undefined) && intersectsLines.length > 0 && !isSnapped.current && !ispreSnapped.current) { + + ////////// Clicked on a preexisting Line ////////// + + if (visibleIntersect && (intersectsLines[0].object.userData.linePoints[0][3] === CONSTANTS.lineConfig.floorName || intersectsLines[0].object.userData.linePoints[0][3] === CONSTANTS.lineConfig.wallName)) { + let pointColor, lineColor; + if (intersectsLines[0].object.userData.linePoints[0][3] === CONSTANTS.lineConfig.wallName) { + pointColor = CONSTANTS.pointConfig.wallOuterColor; + lineColor = CONSTANTS.lineConfig.wallColor; + } else { + pointColor = CONSTANTS.pointConfig.floorOuterColor; + lineColor = CONSTANTS.lineConfig.floorColor; + } + let IntersectsPoint = new THREE.Vector3(intersects[0].point.x, 0.01, intersects[0].point.z); + if (isAngleSnapped.current && line.current.length > 0 && anglesnappedPoint.current) { + IntersectsPoint = anglesnappedPoint.current; + } + if (visibleIntersect.object instanceof THREE.Mesh) { + const ThroughPoint = (visibleIntersect.object.geometry.parameters.path).getPoints(CONSTANTS.lineConfig.lineIntersectionPoints); + let intersectionPoint = getClosestIntersection(ThroughPoint, IntersectsPoint); + + if (intersectionPoint) { + + const newLines = splitLine(visibleIntersect, intersectionPoint, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, lines, setDeletedLines, floorPlanGroupLine, socket, pointColor, lineColor, intersectsLines[0].object.userData.linePoints[0][3]); + setNewLines([newLines[0], newLines[1]]); + + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.floorName]); + + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + lines.current.push(line.current as Types.Line); + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([newLines[0], newLines[1], line.current]); + onlyFloorline.current.push(line.current as Types.Line); + onlyFloorlines.current.push(onlyFloorline.current); + onlyFloorline.current = []; + + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.floorColor, line.current, floorPlanGroupLine); + + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + return; + } + } + } + } + if (intersects.length > 0 && intersectsLines.length === 0) { + + ////////// Clicked on an empty place or a point ////////// + + let intersectionPoint = intersects[0].point; + + if (isAngleSnapped.current && line.current.length > 0 && anglesnappedPoint.current) { + intersectionPoint = anglesnappedPoint.current; + } + if (isSnapped.current && line.current.length > 0 && snappedPoint.current) { + intersectionPoint = snappedPoint.current; + } + if (ispreSnapped.current && snappedPoint.current) { + intersectionPoint = snappedPoint.current; + } + + if (!isSnapped.current && !ispreSnapped.current) { + addPointToScene(intersectionPoint, CONSTANTS.pointConfig.floorOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, CONSTANTS.lineConfig.floorName); + } else { + ispreSnapped.current = false; + isSnapped.current = false; + } + + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.floorName]); + + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + onlyFloorline.current.push(line.current as Types.Line); + lines.current.push(line.current as Types.Line); + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]); + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.floorColor, line.current, floorPlanGroupLine); + const lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + } + if (isSnapped.current) { ////////// Add this to stop the drawing mode after snapping ////////// + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + onlyFloorlines.current.push(onlyFloorline.current); + onlyFloorline.current = []; + } + } +} + +export default drawOnlyFloor; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/floors/loadFloor.ts b/app/src/modules/builder/geomentries/floors/loadFloor.ts new file mode 100644 index 0000000..30efc62 --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/loadFloor.ts @@ -0,0 +1,50 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import addRoofToScene from '../roofs/addRoofToScene'; + +import * as Types from "../../../../types/world/worldTypes"; +import loadOnlyFloors from './loadOnlyFloors'; +import addFloorToScene from './addFloorToScene'; +import getRoomsFromLines from '../lines/getRoomsFromLines'; + +async function loadFloor( + lines: Types.RefLines, + floorGroup: Types.RefGroup, +): Promise { + + if (!floorGroup.current) return; + + floorGroup.current.children = []; + + if (lines.current.length > 2) { + const linesByLayer = lines.current.reduce((acc: { [key: number]: any[] }, pair) => { + const layer = pair[0][2]; + if (!acc[layer]) acc[layer] = []; + acc[layer].push(pair); + return acc; + }, {}); + + for (const layer in linesByLayer) { + // Only Floor Polygons + loadOnlyFloors(floorGroup, linesByLayer, layer); + + const rooms: Types.Rooms = await getRoomsFromLines({ current: linesByLayer[layer] }); + + rooms.forEach(({ coordinates: room, layer }) => { + const userData = room.map(point => point.uuid); + const shape = new THREE.Shape(); + shape.moveTo(room[0].position.x, room[0].position.z); + room.forEach(point => shape.lineTo(point.position.x, point.position.z)); + shape.closePath(); + + // Floor Polygons + addFloorToScene(shape, (layer - 1) * CONSTANTS.wallConfig.height, floorGroup, userData); + + // Roof Polygons + addRoofToScene(shape, (layer - 1) * CONSTANTS.wallConfig.height, userData, floorGroup); + }); + } + } +} + +export default loadFloor; diff --git a/app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts b/app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts new file mode 100644 index 0000000..ca438a2 --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/loadOnlyFloors.ts @@ -0,0 +1,183 @@ +import * as THREE from 'three'; +import * as turf from '@turf/turf'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import * as Types from "../../../../types/world/worldTypes"; + +function loadOnlyFloors( + floorGroup: Types.RefGroup, + linesByLayer: any, + layer: any, +): void { + + ////////// Creating polygon floor based on the onlyFloorlines.current which does not add roof to it, The lines are still stored in Lines.current as well ////////// + + let floorsInLayer = linesByLayer[layer]; + floorsInLayer = floorsInLayer.filter((line: any) => line[0][3] && line[1][3] === CONSTANTS.lineConfig.floorName); + const floorResult = floorsInLayer.map((pair: [THREE.Vector3, string, number, string][]) => + pair.map((point) => ({ + position: [point[0].x, point[0].z], + uuid: point[1] + })) + ); + const FloorLineFeatures = floorResult.map((line: any) => turf.lineString(line.map((p: any) => p.position))); + + function identifyPolygonsAndConnectedLines(FloorLineFeatures: any) { + const floorpolygons = []; + const connectedLines = []; + const unprocessedLines = [...FloorLineFeatures]; // Copy the features + + while (unprocessedLines.length > 0) { + const currentLine = unprocessedLines.pop(); + const coordinates = currentLine.geometry.coordinates; + + // Check if the line is closed (forms a polygon) + if ( + coordinates[0][0] === coordinates[coordinates.length - 1][0] && + coordinates[0][1] === coordinates[coordinates.length - 1][1] + ) { + floorpolygons.push(turf.polygon([coordinates])); // Add as a polygon + continue; + } + + // Check if the line connects to another line + let connected = false; + for (let i = unprocessedLines.length - 1; i >= 0; i--) { + const otherCoordinates = unprocessedLines[i].geometry.coordinates; + + // Check if lines share a start or end point + if ( + coordinates[0][0] === otherCoordinates[otherCoordinates.length - 1][0] && + coordinates[0][1] === otherCoordinates[otherCoordinates.length - 1][1] + ) { + // Merge lines + const mergedCoordinates = [...otherCoordinates, ...coordinates.slice(1)]; + unprocessedLines[i] = turf.lineString(mergedCoordinates); + connected = true; + break; + } else if ( + coordinates[coordinates.length - 1][0] === otherCoordinates[0][0] && + coordinates[coordinates.length - 1][1] === otherCoordinates[0][1] + ) { + // Merge lines + const mergedCoordinates = [...coordinates, ...otherCoordinates.slice(1)]; + unprocessedLines[i] = turf.lineString(mergedCoordinates); + connected = true; + break; + } + } + + if (!connected) { + connectedLines.push(currentLine); // Add unconnected line as-is + } + } + + return { floorpolygons, connectedLines }; + } + + const { floorpolygons, connectedLines } = identifyPolygonsAndConnectedLines(FloorLineFeatures); + + function convertConnectedLinesToPolygons(connectedLines: any) { + return connectedLines.map((line: any) => { + const coordinates = line.geometry.coordinates; + + // If the line has more than two points, close the polygon + if (coordinates.length > 2) { + const firstPoint = coordinates[0]; + const lastPoint = coordinates[coordinates.length - 1]; + + // Check if already closed; if not, close it + if (firstPoint[0] !== lastPoint[0] || firstPoint[1] !== lastPoint[1]) { + coordinates.push(firstPoint); + } + + // Convert the closed line into a polygon + return turf.polygon([coordinates]); + } + + // If not enough points for a polygon, return the line unchanged + return line; + }); + } + + const convertedConnectedPolygons = convertConnectedLinesToPolygons(connectedLines); + + if (convertedConnectedPolygons.length > 0) { + const validPolygons = convertedConnectedPolygons.filter( + (polygon: any) => polygon.geometry?.type === "Polygon" + ); + + if (validPolygons.length > 0) { + floorpolygons.push(...validPolygons); + } + } + + function convertPolygonsToOriginalFormat(floorpolygons: any, originalLines: [THREE.Vector3, string, number, string][][]) { + return floorpolygons.map((polygon: any) => { + const coordinates = polygon.geometry.coordinates[0]; // Extract the coordinates array (assume it's a single polygon) + + // Map each coordinate back to its original structure + const mappedPoints = coordinates.map((coord: [number, number]) => { + const [x, z] = coord; + + // Find the original point matching this coordinate + const originalPoint = originalLines.flat().find(([point]) => point.x === x && point.z === z); + + if (!originalPoint) { + throw new Error(`Original point for coordinate [${x}, ${z}] not found.`); + } + + return originalPoint; + }); + + // Create pairs of consecutive points + const pairs: typeof originalLines = []; + for (let i = 0; i < mappedPoints.length - 1; i++) { + pairs.push([mappedPoints[i], mappedPoints[i + 1]]); + } + + return pairs; + }); + } + + const convertedFloorPolygons: Types.OnlyFloorLines = convertPolygonsToOriginalFormat(floorpolygons, floorsInLayer); + + convertedFloorPolygons.forEach((floor) => { + const points: THREE.Vector3[] = []; + + floor.forEach((lineSegment) => { + const startPoint = lineSegment[0][0]; + points.push(new THREE.Vector3(startPoint.x, startPoint.y, startPoint.z)); + }); + + const lastLine = floor[floor.length - 1]; + const endPoint = lastLine[1][0]; + points.push(new THREE.Vector3(endPoint.x, endPoint.y, endPoint.z)); + + const shape = new THREE.Shape(); + shape.moveTo(points[0].x, points[0].z); + + points.forEach(point => shape.lineTo(point.x, point.z)); + shape.closePath(); + + const extrudeSettings = { + depth: CONSTANTS.floorConfig.height, + bevelEnabled: false + }; + + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const material = new THREE.MeshStandardMaterial({ color: CONSTANTS.floorConfig.defaultColor, side: THREE.DoubleSide }); + const mesh = new THREE.Mesh(geometry, material); + + mesh.castShadow = true; + mesh.receiveShadow = true; + + mesh.position.y = (floor[0][0][2] - 1) * CONSTANTS.wallConfig.height + 0.03; + mesh.rotateX(Math.PI / 2); + mesh.name = `Only_Floor_Line_${floor[0][0][2]}`; + + mesh.userData = floor; + floorGroup?.current?.add(mesh); + }); +} + +export default loadOnlyFloors; diff --git a/app/src/modules/builder/geomentries/floors/updateFloorLines.ts b/app/src/modules/builder/geomentries/floors/updateFloorLines.ts new file mode 100644 index 0000000..6aa5ce8 --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/updateFloorLines.ts @@ -0,0 +1,24 @@ +import * as Types from "../../../../types/world/worldTypes"; + +function updateFloorLines( + onlyFloorlines: Types.RefOnlyFloorLines, + DragedPoint: Types.Mesh | { uuid: string, position: Types.Vector3 } +): void { + + ////////// Update onlyFloorlines.current if it contains the dragged point ////////// + + onlyFloorlines.current.forEach((floorline) => { + floorline.forEach((line) => { + line.forEach((point) => { + const [position, uuid] = point; + if (uuid === DragedPoint.uuid) { + position.x = DragedPoint.position.x; + position.y = 0.01; + position.z = DragedPoint.position.z; + } + }); + }); + }); +} + +export default updateFloorLines; diff --git a/app/src/modules/builder/geomentries/layers/deleteLayer.ts b/app/src/modules/builder/geomentries/layers/deleteLayer.ts new file mode 100644 index 0000000..c8a4e8a --- /dev/null +++ b/app/src/modules/builder/geomentries/layers/deleteLayer.ts @@ -0,0 +1,89 @@ +import { toast } from 'react-toastify'; +import RemoveConnectedLines from '../lines/removeConnectedLines'; + +import * as Types from '../../../../types/world/worldTypes'; +import { Socket } from 'socket.io-client'; +// import { deleteLayer } from '../../../../services/factoryBuilder/lines/deleteLayerApi'; + +async function DeleteLayer( + removedLayer: Types.Number, + lines: Types.RefLines, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroupPoint: Types.RefGroup, + onlyFloorlines: Types.RefOnlyFloorLines, + floorGroup: Types.RefGroup, + setDeletedLines: any, + setRemovedLayer: Types.setRemoveLayerSetState, + socket: Socket +): Promise { + + ////////// Remove the Lines from the lines.current based on the removed layer and rearrange the layer number that are higher than the removed layer ////////// + + const removedLines: Types.Lines = lines.current.filter(line => line[0][2] === removedLayer); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // await deleteLayer(organization, removedLayer); + + //SOCKET + + const data = { + organization: organization, + layer: removedLayer, + socketId: socket.id + } + + socket.emit('v1:Line:delete:layer', data); + + ////////// Remove Points and lines from the removed layer ////////// + + removedLines.forEach((line) => { + line.forEach((removedPoint) => { + RemoveConnectedLines(removedPoint[1], floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, lines); + }); + }); + + ////////// Update the remaining lines layer values in the userData and in lines.current ////////// + + let remaining = lines.current.filter(line => line[0][2] !== removedLayer); + let updatedLines: Types.Lines = []; + remaining.forEach(line => { + let newLines: Types.Line = [...line]; + if (newLines[0][2] > removedLayer) { + newLines[0][2] -= 1; + newLines[1][2] -= 1; + } + + const matchingLine = floorPlanGroupLine.current.children.find(l => l.userData.linePoints[0][1] === line[0][1] && l.userData.linePoints[1][1] === line[1][1]); + if (matchingLine) { + const updatedUserData = matchingLine.userData; + updatedUserData.linePoints[0][2] = newLines[0][2]; + updatedUserData.linePoints[1][2] = newLines[1][2]; + } + updatedLines.push(newLines); + }); + + lines.current = updatedLines; + localStorage.setItem("Lines", JSON.stringify(lines.current)); + + ////////// Also remove OnlyFloorLines and update it in localstorage ////////// + + onlyFloorlines.current = onlyFloorlines.current.filter((floor) => { + return floor[0][0][2] !== removedLayer; + }); + const meshToRemove: any = floorGroup.current?.children.find((mesh) => + mesh.name === `Only_Floor_Line_${removedLayer}` + ); + if (meshToRemove) { + (meshToRemove.material).dispose(); + (meshToRemove.geometry).dispose(); + floorGroup.current?.remove(meshToRemove); + } + + toast.success("Layer Removed!"); + setRemovedLayer(null); +} +export default DeleteLayer; diff --git a/app/src/modules/builder/geomentries/layers/layer2DVisibility.ts b/app/src/modules/builder/geomentries/layers/layer2DVisibility.ts new file mode 100644 index 0000000..3164de9 --- /dev/null +++ b/app/src/modules/builder/geomentries/layers/layer2DVisibility.ts @@ -0,0 +1,35 @@ +import * as Types from "../../../../types/world/worldTypes"; + +function Layer2DVisibility( + activeLayer: Types.Number, + floorPlanGroup: Types.RefGroup, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroupPoint: Types.RefGroup, + currentLayerPoint: Types.RefMeshArray, + dragPointControls: Types.RefDragControl +): void { + + if (floorPlanGroup.current && dragPointControls.current) { + currentLayerPoint.current = []; + floorPlanGroupLine.current.children.forEach((line) => { + const linePoints = line.userData.linePoints; + + const point1 = floorPlanGroupPoint.current.getObjectByProperty('uuid', linePoints[0][1]) as Types.Mesh; + const point2 = floorPlanGroupPoint.current.getObjectByProperty('uuid', linePoints[1][1]) as Types.Mesh; + + if (linePoints[0][2] !== activeLayer && linePoints[1][2] !== activeLayer) { + point1.visible = false; + point2.visible = false; + line.visible = false; + } else { + point1.visible = true; + point2.visible = true; + line.visible = true; + currentLayerPoint.current.push(point1, point2); + } + }); + dragPointControls.current!.objects = currentLayerPoint.current; + } +} + +export default Layer2DVisibility; diff --git a/app/src/modules/builder/geomentries/lines/addLineToScene.ts b/app/src/modules/builder/geomentries/lines/addLineToScene.ts new file mode 100644 index 0000000..79f2a9a --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/addLineToScene.ts @@ -0,0 +1,24 @@ +import * as THREE from "three"; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import * as Types from "../../../../types/world/worldTypes"; + +function addLineToScene( + start: Types.Vector3, + end: Types.Vector3, + colour: Types.Color, + userData: Types.UserData, + floorPlanGroupLine: Types.RefGroup +): void { + + ////////// A function that creates and adds lines based on the start, end, and colour from the params, Also adds the userData in the mesh userData ////////// + + const path = new THREE.CatmullRomCurve3([start, end]); + const geometry = new THREE.TubeGeometry(path, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false); + const material = new THREE.MeshBasicMaterial({ color: colour }); + const mesh = new THREE.Mesh(geometry, material); + floorPlanGroupLine.current.add(mesh); + + mesh.userData.linePoints = userData; +} + +export default addLineToScene; diff --git a/app/src/modules/builder/geomentries/lines/createAndMoveReferenceLine.ts b/app/src/modules/builder/geomentries/lines/createAndMoveReferenceLine.ts new file mode 100644 index 0000000..bcb8e75 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/createAndMoveReferenceLine.ts @@ -0,0 +1,98 @@ +import * as THREE from "three"; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import * as Types from "../../../../types/world/worldTypes"; + +function createAndMoveReferenceLine( + point: Types.Vector3, + cursorPosition: Types.Vector3, + isSnapped: Types.RefBoolean, + ispreSnapped: Types.RefBoolean, + line: Types.RefLine, + setRefTextUpdate: Types.NumberIncrementState, + floorPlanGroup: Types.RefGroup, + ReferenceLineMesh: Types.RefMesh, + LineCreated: Types.RefBoolean, + Tube: Types.RefTubeGeometry, + anglesnappedPoint: Types.RefVector3, + isAngleSnapped: Types.RefBoolean +): void { + + ////////// Creating new and maintaining the old reference line and also snap the reference line based on its angle ////////// + + const startPoint = point; + + const dx = cursorPosition.x - startPoint.x; + const dz = cursorPosition.z - startPoint.z; + let angle = Math.atan2(dz, dx); + + angle = (angle * 180) / Math.PI; + angle = (angle + 360) % 360; + + const snapAngles = [0, 90, 180, 270, 360]; + const snapThreshold = 2.5; + + const closestSnapAngle = snapAngles.reduce((prev, curr) => + Math.abs(curr - angle) < Math.abs(prev - angle) ? curr : prev + ); + + if (!isSnapped.current && !ispreSnapped.current && line.current.length > 0) { + if (Math.abs(closestSnapAngle - angle) <= snapThreshold) { + const snappedAngleRad = (closestSnapAngle * Math.PI) / 180; + const distance = Math.sqrt(dx * dx + dz * dz); + const snappedX = startPoint.x + distance * Math.cos(snappedAngleRad); + const snappedZ = startPoint.z + distance * Math.sin(snappedAngleRad); + + if ( + cursorPosition.distanceTo( + new THREE.Vector3(snappedX, 0.01, snappedZ) + ) < 2 + ) { + cursorPosition.set(snappedX, 0.01, snappedZ); + isAngleSnapped.current = true; + anglesnappedPoint.current = new THREE.Vector3( + snappedX, + 0.01, + snappedZ + ); + } else { + isAngleSnapped.current = false; + anglesnappedPoint.current = null; + } + } else { + isAngleSnapped.current = false; + anglesnappedPoint.current = null; + } + } else { + isAngleSnapped.current = false; + anglesnappedPoint.current = null; + } + + if (!LineCreated.current) { + setRefTextUpdate((prevUpdate) => prevUpdate - 1); + const path = new THREE.LineCurve3(startPoint, cursorPosition); + Tube.current = new THREE.TubeGeometry(path, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false); + const material = new THREE.MeshBasicMaterial({ color: CONSTANTS.lineConfig.helperColor }); + ReferenceLineMesh.current = new THREE.Mesh(Tube.current, material); + ReferenceLineMesh.current.name = CONSTANTS.lineConfig.referenceName; + ReferenceLineMesh.current.userData = { + linePoints: { startPoint, cursorPosition }, + }; + floorPlanGroup.current?.add(ReferenceLineMesh.current); + LineCreated.current = true; + } else { + if (ReferenceLineMesh.current) { + const path = new THREE.LineCurve3(startPoint, new THREE.Vector3(cursorPosition.x, 0.01, cursorPosition.z)); + Tube.current = new THREE.TubeGeometry(path, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false); + + if (ReferenceLineMesh.current) { + ReferenceLineMesh.current.userData = { + linePoints: { startPoint, cursorPosition }, + }; + ReferenceLineMesh.current.geometry.dispose(); + ReferenceLineMesh.current.geometry = Tube.current; + } + } + } +} + +export default createAndMoveReferenceLine; diff --git a/app/src/modules/builder/geomentries/lines/deleteLine.ts b/app/src/modules/builder/geomentries/lines/deleteLine.ts new file mode 100644 index 0000000..66b78a0 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/deleteLine.ts @@ -0,0 +1,88 @@ +import { Socket } from "socket.io-client"; +// import { deleteLineApi } from "../../../../services/factoryBuilder/lines/deleteLineApi"; +import * as Types from "../../../../types/world/worldTypes"; + +import { toast } from 'react-toastify'; + +function deleteLine( + hoveredDeletableLine: Types.RefMesh, + onlyFloorlines: Types.RefOnlyFloorLines, + lines: Types.RefLines, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroupPoint: Types.RefGroup, + setDeletedLines: any, + socket: Socket +): void { + + ////////// Deleting a line and the points if they are not connected to any other line ////////// + + if (!hoveredDeletableLine.current) { + return; + } + + const linePoints = hoveredDeletableLine.current.userData.linePoints; + const connectedpoints = [linePoints[0][1], linePoints[1][1]]; + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // deleteLineApi( + // organization, + // [ + // { "uuid": linePoints[0][1] }, + // { "uuid": linePoints[1][1] } + // ] + // ) + + //SOCKET + + const data = { + organization: organization, + line: [ + { "uuid": linePoints[0][1] }, + { "uuid": linePoints[1][1] } + ], + socketId: socket.id + } + + socket.emit('v1:Line:delete', data); + + + onlyFloorlines.current = onlyFloorlines.current.map(floorline => + floorline.filter(line => line[0][1] !== connectedpoints[0] && line[1][1] !== connectedpoints[1]) + ).filter(floorline => floorline.length > 0); + + lines.current = lines.current.filter(item => item !== linePoints); + (hoveredDeletableLine.current.material).dispose(); + (hoveredDeletableLine.current.geometry).dispose(); + floorPlanGroupLine.current.remove(hoveredDeletableLine.current); + setDeletedLines([linePoints]); + + connectedpoints.forEach((pointUUID) => { + let isConnected = false; + floorPlanGroupLine.current.children.forEach((line) => { + const linePoints = line.userData.linePoints; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + if (uuid1 === pointUUID || uuid2 === pointUUID) { + isConnected = true; + } + }); + + if (!isConnected) { + floorPlanGroupPoint.current.children.forEach((point: any) => { + if (point.uuid === pointUUID) { + (point.material).dispose(); + (point.geometry).dispose(); + floorPlanGroupPoint.current.remove(point); + } + }); + } + }); + + toast.success("Line Removed!"); +} + +export default deleteLine; diff --git a/app/src/modules/builder/geomentries/lines/distanceText.tsx b/app/src/modules/builder/geomentries/lines/distanceText.tsx new file mode 100644 index 0000000..028d835 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/distanceText.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react" +import { getLines } from "../../../../services/factoryBuilder/lines/getLinesApi"; +import * as THREE from "three"; +import { useActiveLayer, useDeletedLines, useNewLines, useToggleView } from "../../../../store/store"; +import objectLinesToArray from "./lineConvertions/objectLinesToArray"; +import { Html } from "@react-three/drei"; +import * as Types from "../../../../types/world/worldTypes"; + +const DistanceText = () => { + const [lines, setLines] = useState<{ distance: string; position: THREE.Vector3; userData: Types.Line; layer: string }[]>([]); + const { activeLayer } = useActiveLayer(); + const { toggleView } = useToggleView(); + const { newLines, setNewLines } = useNewLines(); + const { deletedLines, setDeletedLines } = useDeletedLines(); + + useEffect(() => { + const email = localStorage.getItem('email') + if (!email) return; + const organization = (email.split("@")[1]).split(".")[0]; + + getLines(organization).then((data) => { + data = objectLinesToArray(data); + + const lines = data.filter((line: Types.Line) => line[0][2] === activeLayer) + .map((line: Types.Line) => { + const point1 = new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z); + const point2 = new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z); + const distance = point1.distanceTo(point2); + const midpoint = new THREE.Vector3().addVectors(point1, point2).divideScalar(2); + return { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + }); + setLines(lines) + }) + }, [activeLayer]) + + useEffect(() => { + if (newLines.length > 0) { + if (newLines[0][0][2] !== activeLayer) return; + const newLinesData = newLines.map((line: Types.Line) => { + const point1 = new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z); + const point2 = new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z); + const distance = point1.distanceTo(point2); + const midpoint = new THREE.Vector3().addVectors(point1, point2).divideScalar(2); + + return { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer, + }; + }); + setLines((prevLines) => [...prevLines, ...newLinesData]); + setNewLines([]); + } + }, [newLines, activeLayer]); + + + useEffect(() => { + if ((deletedLines as Types.Lines).length > 0) { + setLines((prevLines) => + prevLines.filter( + (line) => !deletedLines.some((deletedLine: any) => deletedLine[0][1] === line.userData[0][1] && deletedLine[1][1] === line.userData[1][1]) + ) + ); + setDeletedLines([]); + } + }, [deletedLines]); + + return ( + <> + {toggleView && ( + + {lines.map((text) => ( + +
{text.distance} m
+ + ))} +
+ )} + + ) + +} + +export default DistanceText; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/lines/drawWall.ts b/app/src/modules/builder/geomentries/lines/drawWall.ts new file mode 100644 index 0000000..027e2e9 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/drawWall.ts @@ -0,0 +1,167 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +import addPointToScene from '../points/addPointToScene'; +import addLineToScene from './addLineToScene'; +import splitLine from './splitLine'; +import removeReferenceLine from './removeReferenceLine'; +import getClosestIntersection from './getClosestIntersection'; + +import * as Types from "../../../../types/world/worldTypes"; +import arrayLineToObject from './lineConvertions/arrayLineToObject'; +// import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi'; +import { Socket } from 'socket.io-client'; + +async function drawWall( + raycaster: THREE.Raycaster, + plane: Types.RefMesh, + floorPlanGroupPoint: Types.RefGroup, + snappedPoint: Types.RefVector3, + isSnapped: Types.RefBoolean, + isSnappedUUID: Types.RefString, + line: Types.RefLine, + ispreSnapped: Types.RefBoolean, + anglesnappedPoint: Types.RefVector3, + isAngleSnapped: Types.RefBoolean, + lines: Types.RefLines, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroup: Types.RefGroup, + ReferenceLineMesh: Types.RefMesh, + LineCreated: Types.RefBoolean, + currentLayerPoint: Types.RefMeshArray, + dragPointControls: Types.RefDragControl, + setNewLines: any, + setDeletedLines: any, + activeLayer: Types.Number, + socket: Socket +): Promise { + + ////////// Creating lines Based on the positions clicked ////////// + + ////////// Allows the user lines that represents walls and roof, floor if forms a polygon ////////// + + + if (!plane.current) return + let intersects = raycaster.intersectObject(plane.current, true); + + let intersectsLines = raycaster.intersectObjects(floorPlanGroupLine.current.children, true); + let intersectsPoint = raycaster.intersectObjects(floorPlanGroupPoint.current.children, true); + + const VisibleintersectsPoint = intersectsPoint.find(intersect => intersect.object.visible); + const visibleIntersect = intersectsLines.find(intersect => intersect.object.visible && intersect.object.name !== CONSTANTS.lineConfig.referenceName && intersect.object.userData.linePoints[0][3] === CONSTANTS.lineConfig.wallName); + + if ((intersectsPoint.length === 0 || VisibleintersectsPoint === undefined) && intersectsLines.length > 0 && !isSnapped.current && !ispreSnapped.current) { + + ////////// Clicked on a preexisting Line ////////// + + if (visibleIntersect && intersects) { + let IntersectsPoint = new THREE.Vector3(intersects[0].point.x, 0.01, intersects[0].point.z); + + if (isAngleSnapped.current && anglesnappedPoint.current) { + IntersectsPoint = anglesnappedPoint.current; + } + if (visibleIntersect.object instanceof THREE.Mesh) { + const ThroughPoint = (visibleIntersect.object.geometry.parameters.path).getPoints(CONSTANTS.lineConfig.lineIntersectionPoints); + let intersectionPoint = getClosestIntersection(ThroughPoint, IntersectsPoint); + + if (intersectionPoint) { + + const newLines = splitLine(visibleIntersect, intersectionPoint, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, lines, setDeletedLines, floorPlanGroupLine, socket, CONSTANTS.pointConfig.wallOuterColor, CONSTANTS.lineConfig.wallColor, CONSTANTS.lineConfig.wallName); + setNewLines([newLines[0], newLines[1]]); + + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.wallName,]); + + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([newLines[0], newLines[1], line.current]); + lines.current.push(line.current as Types.Line); + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.wallColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + } + return; + } + } + } + } + + if (intersects && intersects.length > 0) { + + ////////// Clicked on a emply place or a point ////////// + + let intersectionPoint = intersects[0].point; + + if (isAngleSnapped.current && line.current.length > 0 && anglesnappedPoint.current) { + intersectionPoint = anglesnappedPoint.current; + } + if (isSnapped.current && line.current.length > 0 && snappedPoint.current) { + intersectionPoint = snappedPoint.current; + } + if (ispreSnapped.current && snappedPoint.current) { + intersectionPoint = snappedPoint.current; + } + + if (!isSnapped.current && !ispreSnapped.current) { + addPointToScene(intersectionPoint, CONSTANTS.pointConfig.wallOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, CONSTANTS.lineConfig.wallName); + } else { + ispreSnapped.current = false; + isSnapped.current = false; + } + + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), isSnappedUUID.current!, activeLayer, CONSTANTS.lineConfig.wallName,]); + + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]) + lines.current.push(line.current as Types.Line); + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.wallColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + } + if (isSnapped.current) { + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + } +} + +export default drawWall; diff --git a/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts b/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts new file mode 100644 index 0000000..41d4ecf --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts @@ -0,0 +1,26 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function getClosestIntersection( + intersects: Types.Vector3Array, + point: Types.Vector3 +): Types.Vector3 | null { + + ////////// A function that finds which point is closest from the intersects points that is given, Used in finding which point in a line is closest when clicked on a line during drawing ////////// + + let closestNewPoint: THREE.Vector3 | null = null; + let minDistance = Infinity; + + for (const intersect of intersects) { + const distance = point.distanceTo(intersect); + if (distance < minDistance) { + minDistance = distance; + closestNewPoint = intersect; + } + } + + return closestNewPoint; +} + +export default getClosestIntersection; diff --git a/app/src/modules/builder/geomentries/lines/getRoomsFromLines.ts b/app/src/modules/builder/geomentries/lines/getRoomsFromLines.ts new file mode 100644 index 0000000..d521252 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/getRoomsFromLines.ts @@ -0,0 +1,86 @@ +import * as THREE from 'three'; +import * as turf from '@turf/turf'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import * as Types from "../../../../types/world/worldTypes"; + +async function getRoomsFromLines(lines: Types.RefLines) { + const rooms: Types.Rooms = []; + + if (lines.current.length > 2) { + const linesByLayer = lines.current.reduce((acc: { [key: number]: any[] }, pair) => { + const layer = pair[0][2]; + if (!acc[layer]) acc[layer] = []; + acc[layer].push(pair); + return acc; + }, {}); + + ////////// Use turf.polygonize to create polygons from the line points ////////// + + for (const layer in linesByLayer) { + + let linesInLayer = linesByLayer[layer]; + linesInLayer = linesInLayer.filter(line => line[0][3] && line[1][3] === CONSTANTS.lineConfig.wallName); + const result = linesInLayer.map((pair: [THREE.Vector3, string, number, string][]) => + pair.map((point) => ({ + position: [point[0].x, point[0].z], + uuid: point[1] + })) + ); + const lineFeatures = result.map(line => turf.lineString(line.map(p => p.position))); + const polygons = turf.polygonize(turf.featureCollection(lineFeatures)); + + let union: any[] = []; + + polygons.features.forEach((feature) => { + union.push(feature); + }); + + if (union.length > 1) { + const unionResult = turf.union(turf.featureCollection(union)); + if (unionResult?.geometry.type === "MultiPolygon") { + unionResult?.geometry.coordinates.forEach((poly) => { + const Coordinates = poly[0].map(([x, z]) => { + const matchingPoint = result.flat().find(r => + r.position[0].toFixed(10) === x.toFixed(10) && + r.position[1].toFixed(10) === z.toFixed(10) + ); + return { + position: new THREE.Vector3(x, 0, z), + uuid: matchingPoint ? matchingPoint.uuid : '' + }; + }); + rooms.push({ coordinates: Coordinates.reverse(), layer: parseInt(layer) }); + }); + } else if (unionResult?.geometry.type === "Polygon") { + const Coordinates = unionResult?.geometry.coordinates[0].map(([x, z]) => { + const matchingPoint = result.flat().find(r => + r.position[0].toFixed(10) === x.toFixed(10) && + r.position[1].toFixed(10) === z.toFixed(10) + ); + return { + position: new THREE.Vector3(x, 0, z), + uuid: matchingPoint ? matchingPoint.uuid : '' + }; + }); + rooms.push({ coordinates: Coordinates.reverse(), layer: parseInt(layer) }); + } + } else if (union.length === 1) { + const Coordinates = union[0].geometry.coordinates[0].map(([x, z]: [number, number]) => { + const matchingPoint = result.flat().find(r => + r.position[0].toFixed(10) === x.toFixed(10) && + r.position[1].toFixed(10) === z.toFixed(10) + ); + return { + position: new THREE.Vector3(x, 0, z), + uuid: matchingPoint ? matchingPoint.uuid : '' + }; + }); + rooms.push({ coordinates: Coordinates, layer: parseInt(layer) }); + } + } + } + + return rooms; +} + +export default getRoomsFromLines; diff --git a/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLineToObject.ts b/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLineToObject.ts new file mode 100644 index 0000000..d38b18a --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLineToObject.ts @@ -0,0 +1,24 @@ +import * as Types from "../../../../../types/world/worldTypes"; + +export default function arrayLineToObject(array: Types.Line) { + if (!Array.isArray(array)) { + return {}; + } + + // Extract common properties from the first point + const commonLayer = array[0][2]; + const commonType = array[0][3]; + + // Map points into a structured format + const line = array.map(([position, uuid]) => ({ + position, + uuid, + })); + + // Create the final structured object + return { + layer: commonLayer, + type: commonType, + line, + }; +} \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLinesToObject.ts b/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLinesToObject.ts new file mode 100644 index 0000000..e41cf4c --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLinesToObject.ts @@ -0,0 +1,30 @@ +import * as Types from "../../../../../types/world/worldTypes"; + +export default function arrayLinesToObject(array: Array) { + if (!Array.isArray(array)) { + return []; + } + + return array.map((lineArray) => { + if (!Array.isArray(lineArray)) { + return null; + } + + // Extract common properties from the first point + const commonLayer = lineArray[0][2]; + const commonType = lineArray[0][3]; + + // Map points into a structured format + const line = lineArray.map(([position, uuid]) => ({ + position, + uuid, + })); + + // Create the final structured object + return { + layer: commonLayer, + type: commonType, + line, + }; + }).filter((item) => item !== null); // Filter out invalid entries +} diff --git a/app/src/modules/builder/geomentries/lines/lineConvertions/objectLineToArray.ts b/app/src/modules/builder/geomentries/lines/lineConvertions/objectLineToArray.ts new file mode 100644 index 0000000..a6ee7ea --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/lineConvertions/objectLineToArray.ts @@ -0,0 +1,13 @@ +import * as THREE from 'three'; + +export default function objectLineToArray(structuredObject: any) { + if (!structuredObject || !structuredObject.line) { + return []; + } + + // Destructure common properties + const { layer, type, line } = structuredObject; + + // Map points back to the original array format + return line.map(({ position, uuid }: any) => [new THREE.Vector3(position.x, position.y, position.z), uuid, layer, type]); +} \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/lines/lineConvertions/objectLinesToArray.ts b/app/src/modules/builder/geomentries/lines/lineConvertions/objectLinesToArray.ts new file mode 100644 index 0000000..7f84195 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/lineConvertions/objectLinesToArray.ts @@ -0,0 +1,20 @@ +import * as THREE from 'three'; + +export default function objectLinesToArray(structuredObjects: any): any { + if (!Array.isArray(structuredObjects)) { + return []; + } + + return structuredObjects.map((structuredObject) => { + if (!structuredObject || !structuredObject.line) { + return []; + } + + const { layer, type, line } = structuredObject; + + return line.map(({ position, uuid }: any) => { + const vector = new THREE.Vector3(position.x, position.y, position.z); + return [vector, uuid, layer, type]; + }); + }); +} diff --git a/app/src/modules/builder/geomentries/lines/referenceDistanceText.tsx b/app/src/modules/builder/geomentries/lines/referenceDistanceText.tsx new file mode 100644 index 0000000..b1ea8db --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/referenceDistanceText.tsx @@ -0,0 +1,48 @@ +import * as THREE from 'three'; +import { Html } from '@react-three/drei'; +import { useState, useEffect } from 'react'; +import { useActiveLayer } from '../../../../store/store'; + +const ReferenceDistanceText = ({ line }: { line: any }) => { + interface TextState { + distance: string; + position: THREE.Vector3; + userData: any; + layer: any; + } + + const [text, setTexts] = useState(null); + const { activeLayer } = useActiveLayer(); + + useEffect(() => { + if (line) { + if (line.parent === null) { + setTexts(null); + return; + } + const distance = line.userData.linePoints.cursorPosition.distanceTo(line.userData.linePoints.startPoint); + const midpoint = new THREE.Vector3().addVectors(line.userData.linePoints.cursorPosition, line.userData.linePoints.startPoint).divideScalar(2); + const newTexts = { + distance: distance.toFixed(1), + position: midpoint, + userData: line, + layer: activeLayer + }; + setTexts(newTexts); + } + }); + + return ( + + + {text !== null && + < Html transform sprite key={text.distance} userData={text.userData} scale={5} position={[text.position.x, 1, text.position.z]} style={{ pointerEvents: 'none' }}> +
{text.distance} m
+ + } +
+
+ ); +}; + +export default ReferenceDistanceText; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/lines/removeConnectedLines.ts b/app/src/modules/builder/geomentries/lines/removeConnectedLines.ts new file mode 100644 index 0000000..f1399a5 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/removeConnectedLines.ts @@ -0,0 +1,66 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function RemoveConnectedLines( + DeletedPointUUID: Types.String, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroupPoint: Types.RefGroup, + setDeletedLines: any, + lines: Types.RefLines, +): void { + + ////////// Check if any and how many lines are connected to the deleted point ////////// + + const removableLines: THREE.Mesh[] = []; + const connectedpoints: string[] = []; + + const removedLinePoints: [number, string, number][][] = []; // Array to hold linePoints of removed lines + + floorPlanGroupLine.current.children.forEach((line) => { + const linePoints = line.userData.linePoints as [number, string, number][]; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + + if (uuid1 === DeletedPointUUID || uuid2 === DeletedPointUUID) { + connectedpoints.push(uuid1 === DeletedPointUUID ? uuid2 : uuid1); + removableLines.push(line as THREE.Mesh); + removedLinePoints.push(linePoints); + } + }); + + if (removableLines.length > 0) { + removableLines.forEach((line) => { + lines.current = lines.current.filter(item => item !== line.userData.linePoints); + (line.material).dispose(); + (line.geometry).dispose(); + floorPlanGroupLine.current.remove(line); + }); + } + setDeletedLines(removedLinePoints) + + ////////// Check and Remove point that are no longer connected to any lines ////////// + + connectedpoints.forEach((pointUUID) => { + let isConnected = false; + floorPlanGroupLine.current.children.forEach((line) => { + const linePoints = line.userData.linePoints as [number, string, number][]; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + if (uuid1 === pointUUID || uuid2 === pointUUID) { + isConnected = true; + } + }); + if (!isConnected) { + floorPlanGroupPoint.current.children.forEach((point: any) => { + if (point.uuid === pointUUID) { + (point.material).dispose(); + (point.geometry).dispose(); + floorPlanGroupPoint.current.remove(point); + } + }); + } + }); +} + +export default RemoveConnectedLines; diff --git a/app/src/modules/builder/geomentries/lines/removeReferenceLine.ts b/app/src/modules/builder/geomentries/lines/removeReferenceLine.ts new file mode 100644 index 0000000..157805c --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/removeReferenceLine.ts @@ -0,0 +1,22 @@ +import * as Types from "../../../../types/world/worldTypes"; + +function removeReferenceLine( + floorPlanGroup: Types.RefGroup, + ReferenceLineMesh: Types.RefMesh, + LineCreated: Types.RefBoolean, + line: Types.RefLine +): void { + + ////////// Removes Dangling reference line if the draw mode is ended or any other case ////////// + + line.current = []; + if (ReferenceLineMesh.current) { + (ReferenceLineMesh.current.material).dispose(); + (ReferenceLineMesh.current.geometry).dispose(); + floorPlanGroup.current.remove(ReferenceLineMesh.current); + LineCreated.current = false; + ReferenceLineMesh.current = undefined; + } +} + +export default removeReferenceLine; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/lines/splitLine.ts b/app/src/modules/builder/geomentries/lines/splitLine.ts new file mode 100644 index 0000000..557db88 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/splitLine.ts @@ -0,0 +1,124 @@ +import * as THREE from 'three'; + +import addLineToScene from './addLineToScene'; +import addPointToScene from '../points/addPointToScene'; + +import * as Types from "../../../../types/world/worldTypes"; +import arrayLineToObject from '../lines/lineConvertions/arrayLineToObject'; +import { Socket } from 'socket.io-client'; +// import { deleteLineApi } from '../../../../services/factoryBuilder/lines/deleteLineApi'; +// import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi'; + +function splitLine( + visibleIntersect: Types.IntersectionEvent, + intersectionPoint: Types.Vector3, + currentLayerPoint: Types.RefMeshArray, + floorPlanGroupPoint: Types.RefGroup, + dragPointControls: Types.RefDragControl, + isSnappedUUID: Types.RefString, + lines: Types.RefLines, + setDeletedLines: any, + floorPlanGroupLine: { current: THREE.Group }, + socket: Socket, + pointColor: Types.String, + lineColor: Types.String, + lineType: Types.String, +): [Types.Line, Types.Line] { + + ////////// Removing the clicked line and splitting it with the clicked position adding a new point and two new lines ////////// + + + ((visibleIntersect.object as any).material).dispose(); + ((visibleIntersect.object as any).geometry).dispose(); + floorPlanGroupLine.current.remove(visibleIntersect.object); + setDeletedLines([visibleIntersect.object.userData.linePoints]); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // deleteLineApi( + // organization, + // [ + // { "uuid": visibleIntersect.object.userData.linePoints[0][1] }, + // { "uuid": visibleIntersect.object.userData.linePoints[1][1] } + // ] + // ) + + //SOCKET + + + const data = { + organization: organization, + line: [ + { "uuid": visibleIntersect.object.userData.linePoints[0][1] }, + { "uuid": visibleIntersect.object.userData.linePoints[1][1] } + ], + socketId: socket.id + } + + socket.emit('v1:Line:delete', data); + + const point = addPointToScene(intersectionPoint, pointColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, isSnappedUUID, lineType); + + const oldLinePoints = visibleIntersect.object.userData.linePoints; + lines.current = lines.current.filter(item => item !== oldLinePoints); + + const clickedPoint: Types.Point = [ + new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), + point.uuid, + oldLinePoints[0][2], + lineType + ]; + + const start = oldLinePoints[0]; + const end = oldLinePoints[1]; + + const newLine1: Types.Line = [start, clickedPoint]; + const newLine2: Types.Line = [clickedPoint, end]; + + const line1 = arrayLineToObject(newLine1); + const line2 = arrayLineToObject(newLine2); + + //REST + + // setLine(organization, line1.layer!, line1.line!, line1.type!); + + //SOCKET + + const input1 = { + organization: organization, + layer: line1.layer, + line: line1.line, + type: line1.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input1); + + //REST + + // setLine(organization, line2.layer!, line2.line!, line2.type!); + + //SOCKET + + const input2 = { + organization: organization, + layer: line2.layer, + line: line2.line, + type: line2.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input2); + + lines.current.push(newLine1, newLine2); + + addLineToScene(newLine1[0][0], newLine1[1][0], lineColor, newLine1, floorPlanGroupLine); + addLineToScene(newLine2[0][0], newLine2[1][0], lineColor, newLine2, floorPlanGroupLine); + + return [newLine1, newLine2]; +} + +export default splitLine; diff --git a/app/src/modules/builder/geomentries/lines/updateDistanceText.ts b/app/src/modules/builder/geomentries/lines/updateDistanceText.ts new file mode 100644 index 0000000..5909ba9 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/updateDistanceText.ts @@ -0,0 +1,42 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function updateDistanceText( + scene: THREE.Scene, + floorPlanGroupLine: Types.RefGroup, + affectedLines: Types.NumberArray +): void { + + ////////// Updating the Distance Texts of the lines that are affected during drag ////////// + + const DistanceGroup = scene.children.find((child) => child.name === "Distance_Text") as THREE.Group; + + affectedLines.forEach((lineIndex) => { + const mesh = floorPlanGroupLine.current.children[lineIndex] as THREE.Mesh; + const linePoints = mesh.userData.linePoints; + + if (linePoints) { + const distance = linePoints[0][0].distanceTo(linePoints[1][0]).toFixed(1); + const position = new THREE.Vector3().addVectors(linePoints[0][0], linePoints[1][0]).divideScalar(2); + + if (!DistanceGroup || !linePoints) { + return + } + + DistanceGroup.children.forEach((text) => { + const textMesh = text as THREE.Mesh; + if (textMesh.userData[0][1] === linePoints[0][1] && textMesh.userData[1][1] === linePoints[1][1]) { + textMesh.position.set(position.x, 1, position.z); + const className = `Distance line-${textMesh.userData[0][1]}_${textMesh.userData[1][1]}_${linePoints[0][2]}`; + const element = document.getElementsByClassName(className)[0] as HTMLElement; + if (element) { + element.innerHTML = `${distance} m`; + } + } + }); + } + }); +} + +export default updateDistanceText; diff --git a/app/src/modules/builder/geomentries/lines/updateLines.ts b/app/src/modules/builder/geomentries/lines/updateLines.ts new file mode 100644 index 0000000..169e452 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/updateLines.ts @@ -0,0 +1,24 @@ +import * as THREE from 'three'; +import * as Types from "../../../../types/world/worldTypes"; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +function updateLines( + floorPlanGroupLine: Types.RefGroup, + affectedLines: Types.NumberArray +): void { + + ////////// Updating the positions for the affected lines only based on the updated positions ////////// + + affectedLines.forEach((lineIndex) => { + const mesh = floorPlanGroupLine.current.children[lineIndex] as Types.Mesh; + const linePoints = mesh.userData.linePoints as Types.Line; + if (linePoints) { + const newPositions = linePoints.map(([pos]) => pos); + const newPath = new THREE.CatmullRomCurve3(newPositions); + mesh.geometry.dispose(); + mesh.geometry = new THREE.TubeGeometry(newPath, CONSTANTS.lineConfig.tubularSegments, CONSTANTS.lineConfig.radius, CONSTANTS.lineConfig.radialSegments, false); + } + }); +} + +export default updateLines; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/lines/updateLinesPositions.ts b/app/src/modules/builder/geomentries/lines/updateLinesPositions.ts new file mode 100644 index 0000000..4297f4a --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/updateLinesPositions.ts @@ -0,0 +1,32 @@ +import * as Types from "../../../../types/world/worldTypes"; + +function updateLinesPositions( + DragedPoint: Types.Mesh | { uuid: string, position: Types.Vector3 }, + lines: Types.RefLines +): Types.NumberArray { + + ////////// Updating the lines position based on the dragged point's position ////////// + + const objectUUID = DragedPoint.uuid; + const affectedLines: Types.NumberArray = []; + + lines.current.forEach((line, index) => { + let lineUpdated = false; + line.forEach((point) => { + const [position, uuid] = point; + if (uuid === objectUUID) { + position.x = DragedPoint.position.x; + position.y = 0.01; + position.z = DragedPoint.position.z; + lineUpdated = true; + } + }); + if (lineUpdated) { + affectedLines.push(index); + } + }); + + return affectedLines; +} + +export default updateLinesPositions; diff --git a/app/src/modules/builder/geomentries/lines/vectorizeLinesCurrent.ts b/app/src/modules/builder/geomentries/lines/vectorizeLinesCurrent.ts new file mode 100644 index 0000000..6bcfd36 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/vectorizeLinesCurrent.ts @@ -0,0 +1,18 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function vectorizeLinesCurrent( + lines: Types.Lines +): Types.Lines { + + ////////// Storing a vector3 array in localstorage makes the prototype functions go puff. This function brings back the prototype functions by creating it again ////////// + + return lines.map((line) => { + const p1: Types.Point = [new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z), line[0][1], line[0][2], line[0][3],]; + const p2: Types.Point = [new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z), line[1][1], line[0][2], line[1][3],]; + return [p1, p2]; + }); +} + +export default vectorizeLinesCurrent; diff --git a/app/src/modules/builder/geomentries/pillars/addAndUpdateReferencePillar.ts b/app/src/modules/builder/geomentries/pillars/addAndUpdateReferencePillar.ts new file mode 100644 index 0000000..a63ad2b --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/addAndUpdateReferencePillar.ts @@ -0,0 +1,54 @@ +import * as THREE from 'three'; +import updateReferencePolesheight from './updateReferencePolesheight'; + +import * as Types from "../../../../types/world/worldTypes"; + +function addAndUpdateReferencePillar( + raycaster: THREE.Raycaster, + floorGroup: Types.RefGroup, + referencePole: Types.RefMesh +): void { + + ////////// Find Pillars position and scale based on the pointer interaction ////////// + + let Roofs = raycaster.intersectObjects(floorGroup.current.children, true); + const intersected = Roofs.find(intersect => intersect.object.name.includes("Roof") || intersect.object.name.includes("Floor")); + + if (intersected) { + const intersectionPoint = intersected.point; + raycaster.ray.origin.copy(intersectionPoint); + raycaster.ray.direction.set(0, -1, 0); + const belowIntersections = raycaster.intersectObjects(floorGroup.current.children, true); + const validIntersections = belowIntersections.filter(intersect => intersect.object.name.includes("Floor")); + + let distance: Types.Number; + + if (validIntersections.length > 1) { + let valid = validIntersections.find(intersectedBelow => intersected.point.distanceTo(intersectedBelow.point) > 3); + if (valid) { + updateReferencePolesheight(intersectionPoint, valid.distance, referencePole, floorGroup); + } else { + const belowPoint = new THREE.Vector3(intersectionPoint.x, 0, intersectionPoint.z); + distance = intersected.point.distanceTo(belowPoint); + if (distance > 3) { + updateReferencePolesheight(intersectionPoint, distance, referencePole, floorGroup); + } + } + } else { + const belowPoint = new THREE.Vector3(intersectionPoint.x, 0, intersectionPoint.z); + distance = intersected.point.distanceTo(belowPoint); + if (distance > 3) { + updateReferencePolesheight(intersectionPoint, distance, referencePole, floorGroup); + } + } + } else { + if (referencePole.current) { + (referencePole.current.material).dispose(); + (referencePole.current.geometry).dispose(); + floorGroup.current.remove(referencePole.current); + referencePole.current = null; + } + } +} + +export default addAndUpdateReferencePillar; diff --git a/app/src/modules/builder/geomentries/pillars/addPillar.ts b/app/src/modules/builder/geomentries/pillars/addPillar.ts new file mode 100644 index 0000000..19144fb --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/addPillar.ts @@ -0,0 +1,24 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import * as Types from "../../../../types/world/worldTypes"; + +function addPillar( + referencePole: Types.RefMesh, + floorGroup: Types.RefGroup +): void { + + ////////// Add Pillars to the scene based on the reference. current poles position and scale ////////// + + if (referencePole.current) { + let pole: THREE.Mesh; + const geometry = referencePole.current.userData.geometry.clone(); + const material = new THREE.MeshStandardMaterial({ color: CONSTANTS.columnConfig.defaultColor }); + pole = new THREE.Mesh(geometry, material); + pole.rotateX(Math.PI / 2); + pole.name = "Pole"; + pole.position.set(referencePole.current.userData.position.x, referencePole.current.userData.position.y, referencePole.current.userData.position.z); + floorGroup.current.add(pole); + } +} + +export default addPillar; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/pillars/deletableHoveredPillar.ts b/app/src/modules/builder/geomentries/pillars/deletableHoveredPillar.ts new file mode 100644 index 0000000..0693766 --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/deletableHoveredPillar.ts @@ -0,0 +1,34 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function DeletableHoveredPillar( + state: Types.ThreeState, + floorGroup: Types.RefGroup, + hoveredDeletablePillar: Types.RefMesh +): void { + + ////////// Altering the color of the hovered Pillar during the Deletion time ////////// + + const intersects = state.raycaster.intersectObjects(floorGroup.current.children, true); + const poleIntersect = intersects.find(intersect => intersect.object.name === "Pole"); + + if (poleIntersect) { + if (poleIntersect.object.name !== "Pole") { + return; + } + if (hoveredDeletablePillar.current) { + (hoveredDeletablePillar.current.material as THREE.MeshStandardMaterial).emissive = new THREE.Color("black"); + hoveredDeletablePillar.current = undefined; + } + hoveredDeletablePillar.current = poleIntersect.object as THREE.Mesh; // Type assertion + (hoveredDeletablePillar.current.material as THREE.MeshStandardMaterial).emissive = new THREE.Color("red"); + } else { + if (hoveredDeletablePillar.current) { + (hoveredDeletablePillar.current.material as THREE.MeshStandardMaterial).emissive = new THREE.Color("black"); + hoveredDeletablePillar.current = undefined; + } + } +} + +export default DeletableHoveredPillar; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/pillars/deletePillar.ts b/app/src/modules/builder/geomentries/pillars/deletePillar.ts new file mode 100644 index 0000000..62b4a2c --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/deletePillar.ts @@ -0,0 +1,21 @@ +import { toast } from 'react-toastify'; + +import * as Types from "../../../../types/world/worldTypes"; + +function DeletePillar( + hoveredDeletablePillar: Types.RefMesh, + floorGroup: Types.RefGroup +): void { + + ////////// Deleting the hovered Pillar from the itemsGroup ////////// + + if (hoveredDeletablePillar.current) { + (hoveredDeletablePillar.current.material).dispose(); + (hoveredDeletablePillar.current.geometry).dispose(); + floorGroup.current.remove(hoveredDeletablePillar.current); + toast.success("Pillar Removed!"); + hoveredDeletablePillar.current = undefined; + } +} + +export default DeletePillar; diff --git a/app/src/modules/builder/geomentries/pillars/updateReferencePolesheight.ts b/app/src/modules/builder/geomentries/pillars/updateReferencePolesheight.ts new file mode 100644 index 0000000..572cc09 --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/updateReferencePolesheight.ts @@ -0,0 +1,40 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function updateReferencePolesheight( + intersectionPoint: Types.Vector3, + distance: Types.Number, + referencePole: Types.RefMesh, + floorGroup: Types.RefGroup +): void { + + ////////// Add a Reference Pillar and update its position and scale based on the pointer interaction ////////// + + if (referencePole.current) { + (referencePole.current.material).dispose(); + (referencePole.current.geometry).dispose(); + floorGroup.current.remove(referencePole.current); + referencePole.current.geometry.dispose(); + } + + const shape = new THREE.Shape(); + shape.moveTo(0.5, 0); + shape.absarc(0, 0, 0.5, 0, 2 * Math.PI, false); + + const extrudeSettings = { + depth: distance, + bevelEnabled: false, + }; + + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const material = new THREE.MeshBasicMaterial({ color: "green", transparent: true, opacity: 0.5 }); + referencePole.current = new THREE.Mesh(geometry, material); + referencePole.current.rotateX(Math.PI / 2); + referencePole.current.position.set(intersectionPoint.x, intersectionPoint.y - 0.01, intersectionPoint.z); + referencePole.current.userData = { geometry: geometry, distance: distance, position: { x: intersectionPoint.x, y: intersectionPoint.y - 0.01, z: intersectionPoint.z } }; + + floorGroup.current.add(referencePole.current); +} + +export default updateReferencePolesheight; diff --git a/app/src/modules/builder/geomentries/points/addPointToScene.ts b/app/src/modules/builder/geomentries/points/addPointToScene.ts new file mode 100644 index 0000000..7a82287 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/addPointToScene.ts @@ -0,0 +1,65 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import * as Types from "../../../../types/world/worldTypes"; + +function addPointToScene( + position: Types.Vector3, + colour: Types.Color, + currentLayerPoint: Types.RefMeshArray, + floorPlanGroupPoint: Types.RefGroup, + dragPointControls: Types.RefDragControl | undefined, + uuid: Types.RefString | undefined, + Type: Types.String +): Types.Mesh { + + ////////// A function that creates and adds a cube (point) with an outline based on the position and colour given as params, It also updates the drag controls objects and sets the box uuid in uuid.current ////////// + + const geometry = new THREE.BoxGeometry(...CONSTANTS.pointConfig.boxScale); + const material = new THREE.ShaderMaterial({ + uniforms: { + uColor: { value: new THREE.Color(colour) }, // Blue color for the border + uInnerColor: { value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor) }, // White color for the inner square + }, + vertexShader: ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform vec3 uColor; + uniform vec3 uInnerColor; + + void main() { + // Define the size of the white square as a proportion of the face + float borderThickness = 0.2; // Adjust this value for border thickness + if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness && + vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) { + gl_FragColor = vec4(uInnerColor, 1.0); // White inner square + } else { + gl_FragColor = vec4(uColor, 1.0); // Blue border + } + } + `, + }); + const point = new THREE.Mesh(geometry, material); + point.name = "point"; + point.userData = { type: Type, color: colour }; + point.position.set(position.x, 0.01, position.z); + + currentLayerPoint.current.push(point); + floorPlanGroupPoint.current.add(point); + if (uuid) { + uuid.current = point.uuid; + } + if (dragPointControls) { + dragPointControls.current!.objects = currentLayerPoint.current; + } + + return point; +} + +export default addPointToScene; diff --git a/app/src/modules/builder/geomentries/points/deletePoint.ts b/app/src/modules/builder/geomentries/points/deletePoint.ts new file mode 100644 index 0000000..9768b21 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/deletePoint.ts @@ -0,0 +1,57 @@ +import * as Types from "../../../../types/world/worldTypes"; + +import { toast } from 'react-toastify'; + +import RemoveConnectedLines from "../lines/removeConnectedLines"; +// import { deletePointApi } from "../../../../services/factoryBuilder/lines/deletePointApi"; +import { Socket } from "socket.io-client"; + +function deletePoint( + hoveredDeletablePoint: Types.RefMesh, + onlyFloorlines: Types.RefOnlyFloorLines, + floorPlanGroupPoint: Types.RefGroup, + floorPlanGroupLine: Types.RefGroup, + lines: Types.RefLines, + setDeletedLines: any, + socket: Socket +): void { + ////////// Deleting a Point and the lines that are connected to it ////////// + + if (!hoveredDeletablePoint.current) { + return; + } + + (hoveredDeletablePoint.current.material).dispose(); + (hoveredDeletablePoint.current.geometry).dispose(); + floorPlanGroupPoint.current.remove(hoveredDeletablePoint.current); + const DeletedPointUUID = hoveredDeletablePoint.current.uuid; + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // deletePointApi(organization, DeletedPointUUID); + + //SOCKET + + const data = { + organization: organization, + uuid: DeletedPointUUID, + socketId: socket.id + } + + socket.emit('v1:Line:delete:point', data); + + ////////// Update onlyFloorlines.current to remove references to the deleted point ////////// + + onlyFloorlines.current = onlyFloorlines.current.map(floorline => + floorline.filter(line => line[0][1] !== DeletedPointUUID && line[1][1] !== DeletedPointUUID) + ).filter(floorline => floorline.length > 0); + + RemoveConnectedLines(DeletedPointUUID, floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, lines); + + toast.success("Point Removed!"); +} + +export default deletePoint; diff --git a/app/src/modules/builder/geomentries/points/dragPoint.ts b/app/src/modules/builder/geomentries/points/dragPoint.ts new file mode 100644 index 0000000..f6aaaa4 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/dragPoint.ts @@ -0,0 +1,44 @@ +import * as THREE from "three"; +import * as Types from "../../../../types/world/worldTypes" +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +import updateLinesPositions from "../lines/updateLinesPositions"; +import updateLines from "../lines/updateLines"; +import updateDistanceText from "../lines/updateDistanceText"; +import updateFloorLines from "../floors/updateFloorLines"; + +function DragPoint( + event: Types.IntersectionEvent, + floorPlanGroupPoint: Types.RefGroup, + floorPlanGroupLine: Types.RefGroup, + scene: THREE.Scene, + lines: Types.RefLines, + onlyFloorlines: Types.RefOnlyFloorLines +): void { + + ////////// Calling the line updation of the affected lines and Snapping of the point during the drag ////////// + + const snapThreshold = CONSTANTS.pointConfig.snappingThreshold; + const DragedPoint = event.object as Types.Mesh; + + floorPlanGroupPoint.current.children.forEach((point) => { + let canSnap = + ((DragedPoint.userData.type === CONSTANTS.lineConfig.wallName) && (point.userData.type === CONSTANTS.lineConfig.wallName || point.userData.type === CONSTANTS.lineConfig.floorName)) || + ((DragedPoint.userData.type === CONSTANTS.lineConfig.floorName) && (point.userData.type === CONSTANTS.lineConfig.wallName || point.userData.type === CONSTANTS.lineConfig.floorName)) || + ((DragedPoint.userData.type === CONSTANTS.lineConfig.aisleName) && point.userData.type === CONSTANTS.lineConfig.aisleName); + if (canSnap && point.uuid !== DragedPoint.uuid && point.visible) { + const distance = DragedPoint.position.distanceTo(point.position); + if (distance < snapThreshold) { + DragedPoint.position.copy(point.position); + } + } + }); + + const affectedLines = updateLinesPositions(DragedPoint, lines); + + updateLines(floorPlanGroupLine, affectedLines); + updateDistanceText(scene, floorPlanGroupLine, affectedLines); + updateFloorLines(onlyFloorlines, DragedPoint); +} + +export default DragPoint; \ No newline at end of file diff --git a/app/src/modules/builder/geomentries/points/removeSoloPoint.ts b/app/src/modules/builder/geomentries/points/removeSoloPoint.ts new file mode 100644 index 0000000..de784f1 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/removeSoloPoint.ts @@ -0,0 +1,37 @@ +import * as Types from "../../../../types/world/worldTypes"; + +function removeSoloPoint( + line: Types.RefLine, + floorPlanGroupLine: Types.RefGroup, + floorPlanGroupPoint: Types.RefGroup +): void { + + ////////// Remove the point if there is only one point and if it is not connected to any other line and also the reference line ////////// + + if (line.current[0]) { + const pointUUID = line.current[0][1]; + let isConnected = false; + + floorPlanGroupLine.current.children.forEach((line) => { + const linePoints = line.userData.linePoints; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + if (uuid1 === pointUUID || uuid2 === pointUUID) { + isConnected = true; + } + }); + + if (!isConnected) { + floorPlanGroupPoint.current.children.forEach((point: any) => { + if (point.uuid === pointUUID) { + (point.material).dispose(); + (point.geometry).dispose(); + floorPlanGroupPoint.current.remove(point); + } + }); + } + line.current = []; + } +} + +export default removeSoloPoint; diff --git a/app/src/modules/builder/geomentries/roofs/addRoofToScene.ts b/app/src/modules/builder/geomentries/roofs/addRoofToScene.ts new file mode 100644 index 0000000..1d872de --- /dev/null +++ b/app/src/modules/builder/geomentries/roofs/addRoofToScene.ts @@ -0,0 +1,32 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import * as Types from "../../../../types/world/worldTypes"; + +function addRoofToScene( + shape: Types.Shape, + floor: Types.Number, + userData: Types.UserData, + floorGroup: Types.RefGroup +): void { + + ////////// Creating a Polygon roof from the shape of the Polygon floor ////////// + + const extrudeSettings: THREE.ExtrudeGeometryOptions = { + depth: CONSTANTS.roofConfig.height, + bevelEnabled: false + }; + + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const material = new THREE.MeshStandardMaterial({ color: CONSTANTS.roofConfig.defaultColor, side: THREE.DoubleSide, transparent: true, depthWrite: false }); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.y = CONSTANTS.wallConfig.height + floor; + mesh.castShadow = true; + mesh.receiveShadow = true; + mesh.rotateX(Math.PI / 2); + mesh.userData.uuids = userData; + mesh.name = `Roof_Layer_${(floor / CONSTANTS.wallConfig.height) + 1}`; + + floorGroup.current.add(mesh); +} + +export default addRoofToScene; diff --git a/app/src/modules/builder/geomentries/roofs/hideRoof.ts b/app/src/modules/builder/geomentries/roofs/hideRoof.ts new file mode 100644 index 0000000..29d65f1 --- /dev/null +++ b/app/src/modules/builder/geomentries/roofs/hideRoof.ts @@ -0,0 +1,47 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function hideRoof( + visibility: Types.Boolean, + floorGroup: Types.RefGroup, + camera: THREE.Camera +): void { + + ////////// Toggles the visibility of the roof based on the camera position and the Roof visibility button on UI ////////// + + const v = new THREE.Vector3(); + const u = new THREE.Vector3(); + + if (visibility === true && floorGroup.current) { + for (const child of floorGroup.current.children) { + if (child.name.includes("Roof")) { + const roofChild = child as Types.Mesh; + roofChild.getWorldDirection(v); + camera?.getWorldDirection(u); + if (roofChild.material) { + const materials = Array.isArray(roofChild.material) ? roofChild.material : [roofChild.material]; + materials.forEach(material => { + material.visible = v.dot(u) < 0.25; + }); + } + } + } + } else { + if (floorGroup.current) { + for (const child of floorGroup.current.children) { + if (child.name.includes("Roof")) { + const roofChild = child as Types.Mesh; + if (roofChild.material) { + const materials = Array.isArray(roofChild.material) ? roofChild.material : [roofChild.material]; + materials.forEach(material => { + material.visible = false; + }); + } + } + } + } + } +} + +export default hideRoof; diff --git a/app/src/modules/builder/geomentries/walls/addWallItems.ts b/app/src/modules/builder/geomentries/walls/addWallItems.ts new file mode 100644 index 0000000..351b375 --- /dev/null +++ b/app/src/modules/builder/geomentries/walls/addWallItems.ts @@ -0,0 +1,108 @@ +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { toast } from 'react-toastify'; + +import * as THREE from 'three'; +import * as Types from "../../../../types/world/worldTypes"; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +// import { setWallItem } from '../../../../services/factoryBuilder/assest/wallAsset/setWallItemApi'; +import { Socket } from 'socket.io-client'; + +async function AddWallItems( + selected: Types.String, + raycaster: THREE.Raycaster, + CSGGroup: Types.RefMesh, + AssetConfigurations: Types.AssetConfigurations, + setWallItems: Types.setWallItemSetState, + socket: Socket +): Promise { + + ////////// Load Wall GLtf's and set the positions, rotation, type etc. in state and store in localstorage ////////// + + let intersects = raycaster?.intersectObject(CSGGroup.current!, true); + const wallRaycastIntersection = intersects?.find((child) => child.object.name.includes("WallRaycastReference")); + + if (wallRaycastIntersection) { + const intersectionPoint = wallRaycastIntersection; + const loader = new GLTFLoader(); + loader.load(AssetConfigurations[selected].modelUrl, async (gltf) => { + const model = gltf.scene; + model.userData = { wall: intersectionPoint.object.parent }; + model.children[0].children.forEach((child) => { + if (child.name !== "CSG_REF") { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const config = AssetConfigurations[selected]; + let positionY = typeof config.positionY === 'function' ? config.positionY(intersectionPoint) : config.positionY; + if (positionY === 0) { + positionY = Math.floor(intersectionPoint.point.y / CONSTANTS.wallConfig.height) * CONSTANTS.wallConfig.height; + } + + const newWallItem = { + type: config.type, + model: model, + modelname: selected, + scale: config.scale, + csgscale: config.csgscale, + csgposition: config.csgposition, + position: [intersectionPoint.point.x, positionY, intersectionPoint.point.z] as [number, number, number], + quaternion: intersectionPoint.object.quaternion.clone() as Types.QuaternionType + }; + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // await setWallItem( + // organization, + // model.uuid, + // newWallItem.modelname, + // newWallItem.type!, + // newWallItem.csgposition!, + // newWallItem.csgscale!, + // newWallItem.position, + // newWallItem.quaternion, + // newWallItem.scale!, + // ) + + //SOCKET + + const data = { + organization: organization, + modeluuid: model.uuid, + modelname: newWallItem.modelname, + type: newWallItem.type!, + csgposition: newWallItem.csgposition!, + csgscale: newWallItem.csgscale!, + position: newWallItem.position, + quaternion: newWallItem.quaternion, + scale: newWallItem.scale!, + socketId: socket.id + } + + socket.emit('v1:wallItems:set', data); + + setWallItems((prevItems) => { + const updatedItems = [...prevItems, newWallItem]; + + const WallItemsForStorage = updatedItems.map(item => { + const { model, ...rest } = item; + return { + ...rest, + modeluuid: model?.uuid, + }; + }); + + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + toast.success("Model Added!"); + + return updatedItems; + }); + }); + } +} + +export default AddWallItems; diff --git a/app/src/modules/builder/geomentries/walls/deleteWallItems.ts b/app/src/modules/builder/geomentries/walls/deleteWallItems.ts new file mode 100644 index 0000000..2f3373b --- /dev/null +++ b/app/src/modules/builder/geomentries/walls/deleteWallItems.ts @@ -0,0 +1,59 @@ +import { toast } from 'react-toastify'; + +import * as Types from "../../../../types/world/worldTypes"; +// import { deleteWallItem } from '../../../../services/factoryBuilder/assest/wallAsset/deleteWallItemApi'; +import { Socket } from 'socket.io-client'; + +function DeleteWallItems( + hoveredDeletableWallItem: Types.RefMesh, + setWallItems: Types.setWallItemSetState, + wallItems: Types.wallItems, + socket: Socket +): void { + + ////////// Deleting the hovered Wall GLTF from thewallItems and also update it in the localstorage ////////// + + if (hoveredDeletableWallItem.current && hoveredDeletableWallItem.current.parent) { + setWallItems([]); + let WallItemsRef = wallItems; + const removedItem = WallItemsRef.find((item) => item.model?.uuid === hoveredDeletableWallItem.current?.parent?.uuid); + const Items = WallItemsRef.filter((item) => item.model?.uuid !== hoveredDeletableWallItem.current?.parent?.uuid); + + setTimeout(async () => { + WallItemsRef = Items; + setWallItems(WallItemsRef); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // await deleteWallItem(organization, removedItem?.model?.uuid!, removedItem?.modelname!) + + //SOCKET + + const data = { + organization: organization, + modeluuid: removedItem?.model?.uuid!, + modelname: removedItem?.modelname!, + socketId: socket.id + } + + socket.emit('v1:wallItems:delete', data); + + const WallItemsForStorage = WallItemsRef.map(item => { + const { model, ...rest } = item; + return { + ...rest, + modeluuid: model?.uuid, + }; + }); + + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + toast.success("Model Removed!"); + hoveredDeletableWallItem.current = null; + }, 50); + } +} + +export default DeleteWallItems; diff --git a/app/src/modules/builder/geomentries/walls/hideWalls.ts b/app/src/modules/builder/geomentries/walls/hideWalls.ts new file mode 100644 index 0000000..bba582e --- /dev/null +++ b/app/src/modules/builder/geomentries/walls/hideWalls.ts @@ -0,0 +1,45 @@ +import * as THREE from 'three'; + +import * as Types from "../../../../types/world/worldTypes"; + +function hideWalls( + visibility: Types.Boolean, + scene: THREE.Scene, + camera: THREE.Camera +): void { + + ////////// Altering the visibility of the Walls when the world direction of the wall is facing the camera ////////// + + const v = new THREE.Vector3(); + const u = new THREE.Vector3(); + + if (visibility === true) { + for (const children of scene.children) { + if (children.name === "Walls" && children.children[0]?.children.length > 0) { + children.children[0].children.forEach((child: any) => { + if (child.children[0]?.userData.WallType === "RoomWall") { + child.children[0].getWorldDirection(v); + camera.getWorldDirection(u); + if (child.children[0].material) { + child.children[0].material.visible = (2 * v.dot(u)) >= -0.5; + } + } + }); + } + } + } else { + for (const children of scene.children) { + if (children.name === "Walls" && children.children[0]?.children.length > 0) { + children.children[0].children.forEach((child: any) => { + if (child.children[0]?.userData.WallType === "RoomWall") { + if (child.children[0].material) { + child.children[0].material.visible = true; + } + } + }); + } + } + } +} + +export default hideWalls; diff --git a/app/src/modules/builder/geomentries/walls/loadWalls.ts b/app/src/modules/builder/geomentries/walls/loadWalls.ts new file mode 100644 index 0000000..0947a6b --- /dev/null +++ b/app/src/modules/builder/geomentries/walls/loadWalls.ts @@ -0,0 +1,129 @@ +import * as THREE from 'three'; +import * as turf from '@turf/turf'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +import * as Types from "../../../../types/world/worldTypes"; +import getRoomsFromLines from '../lines/getRoomsFromLines'; + +async function loadWalls( + lines: Types.RefLines, + setWalls: any, +): Promise { + ////////// Removes the old walls if any, Checks if there is any overlapping in lines if any updates it , starts function that creates floor and roof ////////// + + const Walls: Types.Walls = []; + const Rooms: Types.Rooms = []; + + localStorage.setItem("Lines", JSON.stringify(lines.current)); + + if (lines.current.length > 1) { + + ////////// Add Walls that are forming a room ////////// + + const wallSet = new Set(); + + const rooms: Types.Rooms = await getRoomsFromLines(lines); + Rooms.push(...rooms); + + Rooms.forEach(({ coordinates: room, layer }) => { + for (let i = 0; i < room.length - 1; i++) { + const uuid1 = room[i].uuid; + const uuid2 = room[(i + 1) % room.length].uuid; + const wallId = `${uuid1}_${uuid2}`; + + if (!wallSet.has(wallId)) { + const p1 = room[i].position; + const p2 = room[(i + 1) % room.length].position; + + const shape = new THREE.Shape(); + shape.moveTo(0, 0); + shape.lineTo(0, CONSTANTS.wallConfig.height); + shape.lineTo(p2.distanceTo(p1), CONSTANTS.wallConfig.height); + shape.lineTo(p2.distanceTo(p1), 0); + shape.lineTo(0, 0); + + const extrudeSettings = { + depth: CONSTANTS.wallConfig.width, + bevelEnabled: false + }; + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const angle = Math.atan2(p2.z - p1.z, p2.x - p1.x); + Walls.push([geometry, [0, -angle, 0], [p1.x, (layer - 1) * CONSTANTS.wallConfig.height, p1.z], "RoomWall", layer]); + + wallSet.add(wallId); + } + } + }); + + ////////// Add Walls that are not forming any room ////////// + + lines.current.forEach(line => { + if (line[0][3] && line[1][3] !== CONSTANTS.lineConfig.wallName) { + return; + } + const [uuid1, uuid2] = line.map(point => point[1]); + let isInRoom = false; + const lineLayer = line[0][2]; + + for (let room of Rooms) { + const roomLayer = room.layer; + if (roomLayer !== lineLayer) continue; + for (let i = 0; i < room.coordinates.length - 1; i++) { + const roomUuid1 = room.coordinates[i].uuid; + const roomUuid2 = room.coordinates[(i + 1) % room.coordinates.length].uuid; + if ( + (uuid1 === roomUuid1 && uuid2 === roomUuid2) || + (uuid1 === roomUuid2 && uuid2 === roomUuid1) + ) { + isInRoom = true; + break; + } + } + if (isInRoom) break; + } + + if (!isInRoom) { + const p1 = new THREE.Vector3(line[0][0].x, 0, line[0][0].z); + const p2 = new THREE.Vector3(line[1][0].x, 0, line[1][0].z); + + let isCollinear = false; + for (let room of Rooms) { + if (room.layer !== lineLayer) continue; + for (let i = 0; i < room.coordinates.length - 1; i++) { + const roomP1 = room.coordinates[i].position; + const roomP2 = room.coordinates[(i + 1) % room.coordinates.length].position; + const lineFeature = turf.lineString([[p1.x, p1.z], [p2.x, p2.z]]); + const roomFeature = turf.lineString([[roomP1.x, roomP1.z], [roomP2.x, roomP2.z]]); + if (turf.booleanOverlap(lineFeature, roomFeature)) { + isCollinear = true; + break; + } + } + if (isCollinear) break; + } + + if (!isCollinear) { + const shape = new THREE.Shape(); + shape.moveTo(0, 0); + shape.lineTo(0, CONSTANTS.wallConfig.height); + shape.lineTo(p2.distanceTo(p1), CONSTANTS.wallConfig.height); + shape.lineTo(p2.distanceTo(p1), 0); + shape.lineTo(0, 0); + + const extrudeSettings = { + depth: CONSTANTS.wallConfig.width, + bevelEnabled: false + }; + const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings); + const angle = Math.atan2(p2.z - p1.z, p2.x - p1.x); + Walls.push([geometry, [0, -angle, 0], [p1.x, (lineLayer - 1) * CONSTANTS.wallConfig.height, p1.z], "SegmentWall", lineLayer]); + } + } + }); + setWalls(Walls); + }else{ + setWalls([]); + } +} + +export default loadWalls; diff --git a/app/src/modules/builder/geomentries/zones/addZonesToScene.ts b/app/src/modules/builder/geomentries/zones/addZonesToScene.ts new file mode 100644 index 0000000..04316d6 --- /dev/null +++ b/app/src/modules/builder/geomentries/zones/addZonesToScene.ts @@ -0,0 +1,50 @@ +import * as THREE from 'three'; +import * as Types from '../../../../types/world/worldTypes'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; + +const baseMaterial = new THREE.ShaderMaterial({ + side: THREE.DoubleSide, + vertexShader: ` + varying vec2 vUv; + void main(){ + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + vUv = uv; + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform vec3 uColor; + void main(){ + float alpha = 1.0 - vUv.y; + gl_FragColor = vec4(uColor, alpha); + } + `, + uniforms: { + uColor: { value: new THREE.Color(CONSTANTS.zoneConfig.defaultColor) }, + }, + transparent: true, +}); + +export default function addZonesToScene( + line: Types.Line, + floorGroupZone: Types.RefGroup, + color: THREE.Color +) { + const point1 = line[0][0]; + const point2 = line[1][0]; + + const length = (new THREE.Vector3(point2.x, point2.y, point2.z)).distanceTo(new THREE.Vector3(point1.x, point1.y, point1.z)); + const angle = Math.atan2(point2.z - point1.z, point2.x - point1.x); + + const geometry = new THREE.PlaneGeometry(length, 10); + + const material = baseMaterial.clone(); + material.uniforms.uColor.value.set(color.r, color.g, color.b); + + const mesh = new THREE.Mesh(geometry, material); + + mesh.position.set((point1.x + point2.x) / 2, ((line[0][2] - 1) * CONSTANTS.wallConfig.height) + 5, (point1.z + point2.z) / 2); + mesh.rotation.y = -angle; + + floorGroupZone.current.add(mesh); +} diff --git a/app/src/modules/builder/geomentries/zones/loadZones.ts b/app/src/modules/builder/geomentries/zones/loadZones.ts new file mode 100644 index 0000000..b33aa44 --- /dev/null +++ b/app/src/modules/builder/geomentries/zones/loadZones.ts @@ -0,0 +1,19 @@ +import * as Types from '../../../../types/world/worldTypes'; +import * as THREE from 'three'; +import * as CONSTANTS from '../../../../types/world/worldConstants'; +import addZonesToScene from './addZonesToScene'; + +export default function loadZones( + lines: Types.RefLines, + floorGroupZone: Types.RefGroup +) { + if (!floorGroupZone.current) return + floorGroupZone.current.children = []; + const zones = lines.current.filter((line) => line[0][3] && line[1][3] === CONSTANTS.lineConfig.zoneName); + + if (zones.length > 0) { + zones.forEach((zone: Types.Line) => { + addZonesToScene(zone, floorGroupZone, new THREE.Color(CONSTANTS.zoneConfig.color)) + }) + } +} \ No newline at end of file diff --git a/app/src/modules/builder/groups/floorGroup.tsx b/app/src/modules/builder/groups/floorGroup.tsx new file mode 100644 index 0000000..a9daa2d --- /dev/null +++ b/app/src/modules/builder/groups/floorGroup.tsx @@ -0,0 +1,101 @@ +import { useFrame, useThree } from "@react-three/fiber"; +import { useAddAction, useDeleteModels, useRoofVisibility, useToggleView, useWallVisibility, useUpdateScene } from "../../../store/store"; +import hideRoof from "../geomentries/roofs/hideRoof"; +import hideWalls from "../geomentries/walls/hideWalls"; +import addAndUpdateReferencePillar from "../geomentries/pillars/addAndUpdateReferencePillar"; +import { useEffect } from "react"; +import addPillar from "../geomentries/pillars/addPillar"; +import DeletePillar from "../geomentries/pillars/deletePillar"; +import DeletableHoveredPillar from "../geomentries/pillars/deletableHoveredPillar"; +import loadFloor from "../geomentries/floors/loadFloor"; + +const FloorGroup = ({ floorGroup, lines, referencePole, hoveredDeletablePillar }: any) => { + const state = useThree(); + const { roofVisibility, setRoofVisibility } = useRoofVisibility(); + const { wallVisibility, setWallVisibility } = useWallVisibility(); + const { toggleView, setToggleView } = useToggleView(); + const { scene, camera, pointer, raycaster, gl } = useThree(); + const { addAction, setAddAction } = useAddAction(); + const { deleteModels, setDeleteModels } = useDeleteModels(); + const { updateScene, setUpdateScene } = useUpdateScene(); + + useEffect(() => { + if (updateScene) { + loadFloor(lines, floorGroup); + setUpdateScene(false); + } + }, [updateScene]) + + useEffect(() => { + if (!addAction) { + if (referencePole.current) { + (referencePole.current as any).material.dispose(); + (referencePole.current.geometry as any).dispose(); + floorGroup.current.remove(referencePole.current); + referencePole.current = undefined; + } + } + }, [addAction]); + + useEffect(() => { + const canvasElement = gl.domElement; + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; + + const onMouseUp = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + if (!drag) { + if (addAction === "pillar") { + addPillar(referencePole, floorGroup); + } + if (deleteModels) { + DeletePillar(hoveredDeletablePillar, floorGroup); + } + } + } + }; + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + }; + }, [deleteModels, addAction]) + + useFrame(() => { + hideRoof(roofVisibility, floorGroup, camera); + hideWalls(wallVisibility, scene, camera); + + if (addAction === "pillar") { + addAndUpdateReferencePillar(raycaster, floorGroup, referencePole); + } + if (deleteModels) { + DeletableHoveredPillar(state, floorGroup, hoveredDeletablePillar); + } + }) + + return ( + + + ) +} + +export default FloorGroup; \ No newline at end of file diff --git a/app/src/modules/builder/groups/floorGroupAisle.tsx b/app/src/modules/builder/groups/floorGroupAisle.tsx new file mode 100644 index 0000000..3c32d65 --- /dev/null +++ b/app/src/modules/builder/groups/floorGroupAisle.tsx @@ -0,0 +1,245 @@ +import * as THREE from 'three'; +import * as Types from '../../../types/world/worldTypes'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import { useThree } from "@react-three/fiber"; +import { useToggleView, useActiveLayer, useSocketStore, useDeletePointOrLine, useMovePoint, useUpdateScene, useNewLines, useToolMode } from "../../../store/store"; +import { useEffect } from "react"; +import removeSoloPoint from "../geomentries/points/removeSoloPoint"; +import removeReferenceLine from "../geomentries/lines/removeReferenceLine"; +import getClosestIntersection from "../geomentries/lines/getClosestIntersection"; +import addPointToScene from "../geomentries/points/addPointToScene"; +import arrayLineToObject from '../geomentries/lines/lineConvertions/arrayLineToObject'; +import addLineToScene from "../geomentries/lines/addLineToScene"; +import loadAisles from '../geomentries/aisles/loadAisles'; + + +const FloorGroupAilse = ({ floorGroupAisle, plane, floorPlanGroupLine, floorPlanGroupPoint, line, lines, currentLayerPoint, dragPointControls, floorPlanGroup, ReferenceLineMesh, LineCreated, isSnapped, ispreSnapped, snappedPoint, isSnappedUUID, isAngleSnapped, anglesnappedPoint }: any) => { + const { toggleView, setToggleView } = useToggleView(); + const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); + const { toolMode, setToolMode } = useToolMode(); + const { movePoint, setMovePoint } = useMovePoint(); + const { socket } = useSocketStore(); + const { activeLayer } = useActiveLayer(); + const { gl, raycaster, camera, pointer } = useThree(); + const { updateScene, setUpdateScene } = useUpdateScene(); + const { newLines, setNewLines } = useNewLines(); + + useEffect(() => { + if (updateScene) { + loadAisles(lines, floorGroupAisle); + setUpdateScene(false); + } + }, [updateScene]) + + useEffect(() => { + if (toolMode === "Aisle") { + setDeletePointOrLine(false); + setMovePoint(false); + } else { + removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint); + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + }, [toolMode]); + + useEffect(() => { + + const canvasElement = gl.domElement; + + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; + + const onMouseUp = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + } + } + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + const onContextMenu = (e: any) => { + e.preventDefault(); + if (toolMode === "Aisle") { + removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint); + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + }; + + const onMouseClick = (evt: any) => { + if (!plane.current || drag) return; + + const intersects = raycaster.intersectObject(plane.current, true); + let intersectionPoint = intersects[0].point; + const points = floorPlanGroupPoint.current?.children ?? []; + const intersectsPoint = raycaster.intersectObjects(points, true).find(intersect => intersect.object.visible); + let intersectsLines: any = raycaster.intersectObjects(floorPlanGroupLine.current.children, true); + + + if (intersectsLines.length > 0 && intersects && intersects.length > 0 && !intersectsPoint) { + const lineType = intersectsLines[0].object.userData.linePoints[0][3]; + if (lineType === CONSTANTS.lineConfig.aisleName) { + // console.log("intersected a aisle line"); + const ThroughPoint = (intersectsLines[0].object.geometry.parameters.path).getPoints(CONSTANTS.lineConfig.lineIntersectionPoints); + let intersection = getClosestIntersection(ThroughPoint, intersectionPoint); + if (!intersection) return; + const point = addPointToScene(intersection, CONSTANTS.pointConfig.aisleOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, undefined, CONSTANTS.lineConfig.aisleName); + (line.current as Types.Line).push([new THREE.Vector3(intersection.x, 0.01, intersection.z), point.uuid, activeLayer, CONSTANTS.lineConfig.aisleName,]); + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + lines.current.push(line.current as Types.Line); + + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]); + + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.pointConfig.aisleOuterColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + } + } + } else if (intersectsPoint && intersects && intersects.length > 0) { + if (intersectsPoint.object.userData.type === CONSTANTS.lineConfig.aisleName) { + // console.log("intersected a aisle point"); + intersectionPoint = intersectsPoint.object.position; + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), intersectsPoint.object.uuid, activeLayer, CONSTANTS.lineConfig.aisleName,]); + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + lines.current.push(line.current as Types.Line); + + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]); + + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.pointConfig.aisleOuterColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + ispreSnapped.current = false; + isSnapped.current = false; + } + } + } else if (intersects && intersects.length > 0) { + // console.log("intersected a empty area"); + let uuid: string = ""; + if (isAngleSnapped.current && anglesnappedPoint.current && line.current.length > 0) { + intersectionPoint = anglesnappedPoint.current; + const point = addPointToScene(intersectionPoint, CONSTANTS.pointConfig.aisleOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, undefined, CONSTANTS.lineConfig.aisleName); + uuid = point.uuid; + } else if (isSnapped.current && snappedPoint.current && line.current.length > 0) { + intersectionPoint = snappedPoint.current; + uuid = isSnappedUUID.current!; + } else if (ispreSnapped.current && snappedPoint.current) { + intersectionPoint = snappedPoint.current; + uuid = isSnappedUUID.current!; + } else { + const point = addPointToScene(intersectionPoint, CONSTANTS.pointConfig.aisleOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, undefined, CONSTANTS.lineConfig.aisleName); + uuid = point.uuid; + } + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), uuid, activeLayer, CONSTANTS.lineConfig.aisleName,]); + + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + lines.current.push(line.current as Types.Line); + + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]); + + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.pointConfig.aisleOuterColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + ispreSnapped.current = false; + isSnapped.current = false; + } + } + } + + + if (toolMode === 'Aisle') { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("click", onMouseClick); + canvasElement.addEventListener("contextmenu", onContextMenu); + } + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("click", onMouseClick); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [toolMode]) + + + return ( + + + ) +} + +export default FloorGroupAilse; \ No newline at end of file diff --git a/app/src/modules/builder/groups/floorItemsGroup.tsx b/app/src/modules/builder/groups/floorItemsGroup.tsx new file mode 100644 index 0000000..155f8e8 --- /dev/null +++ b/app/src/modules/builder/groups/floorItemsGroup.tsx @@ -0,0 +1,292 @@ +import { useFrame, useThree } from "@react-three/fiber"; +import { useActiveTool, useCamMode, useDeletableFloorItem, useDeleteModels, useFloorItems, useRenderDistance, useselectedFloorItem, useSelectedItem, useSocketStore, useToggleView, useTransformMode } from "../../../store/store"; +import assetVisibility from "../geomentries/assets/assetVisibility"; +import { useEffect } from "react"; +import * as THREE from "three"; +import * as Types from "../../../types/world/worldTypes"; +import assetManager, { cancelOngoingTasks } from "../geomentries/assets/assetManager"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; +import DeletableHoveredFloorItems from "../geomentries/assets/deletableHoveredFloorItems"; +import DeleteFloorItems from "../geomentries/assets/deleteFloorItems"; +import loadInitialFloorItems from "../../scene/IntialLoad/loadInitialFloorItems"; +import addAssetModel from "../geomentries/assets/addAssetModel"; +// import { getFloorItems } from "../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi"; +// import { retrieveGLTF } from "../../../utils/indexDB/idbUtils"; +const assetManagerWorker = new Worker(new URL('../../../services/factoryBuilder/webWorkers/assetManagerWorker.js', import.meta.url)); +// const gltfLoaderWorker = new Worker(new URL('../../../services/factoryBuilder/webWorkers/gltfLoaderWorker.js', import.meta.url)); + +const FloorItemsGroup = ({ itemsGroup, hoveredDeletableFloorItem, AttachedObject, floorGroup, tempLoader, isTempLoader, plane }: any) => { + const state: Types.ThreeState = useThree(); + const { raycaster, camera, controls, pointer }: any = state; + const { renderDistance, setRenderDistance } = useRenderDistance(); + const { toggleView, setToggleView } = useToggleView(); + const { floorItems, setFloorItems } = useFloorItems(); + const { camMode, setCamMode } = useCamMode(); + const { deleteModels, setDeleteModels } = useDeleteModels(); + const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem(); + const { transformMode, setTransformMode } = useTransformMode(); + const { selectedFloorItem, setselectedFloorItem } = useselectedFloorItem(); + const { activeTool, setActiveTool } = useActiveTool(); + const { selectedItem, setSelectedItem } = useSelectedItem(); + const { socket } = useSocketStore(); + + 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); + + useEffect(() => { + // Load initial floor items + + // const email = localStorage.getItem('email'); + // const organization = (email!.split("@")[1]).split(".")[0]; + + // getFloorItems(organization).then((data) => { + // gltfLoaderWorker.postMessage({ FloorItems: data }) + // }) + + // gltfLoaderWorker.onmessage = async (event) => { + // if (event.data.message === "gltfLoaded" && event.data.modelBlob) { + // const blobUrl = URL.createObjectURL(event.data.modelBlob); + + // loader.load(blobUrl, (gltf) => { + // URL.revokeObjectURL(blobUrl); + // THREE.Cache.remove(blobUrl); + // THREE.Cache.add(event.data.modelID, gltf); + // }); + + // } else if (event.data.message === "done") { + // loadInitialFloorItems(itemsGroup, setFloorItems); + // } + // } + + + loadInitialFloorItems(itemsGroup, setFloorItems); + }, []); + + useEffect(() => { + assetManagerWorker.onmessage = async (event) => { + cancelOngoingTasks(); // Cancel the ongoing process + await assetManager(event.data, itemsGroup, loader); + }; + }, [assetManagerWorker]); + + useEffect(() => { + if (toggleView) return + + const uuids: string[] = []; + itemsGroup.current?.children.forEach((child: any) => { + uuids.push(child.uuid); + }); + const cameraPosition = state.camera.position; + + assetManagerWorker.postMessage({ floorItems, cameraPosition, uuids, renderDistance }); + }, [camMode, renderDistance]); + + useEffect(() => { + const controls: any = state.controls; + const camera: any = state.camera; + + if (controls) { + let intervalId: NodeJS.Timeout | null = null; + + const handleChange = () => { + if (toggleView) return + + const uuids: string[] = []; + itemsGroup.current?.children.forEach((child: any) => { + uuids.push(child.uuid); + }); + const cameraPosition = camera.position; + + assetManagerWorker.postMessage({ floorItems, cameraPosition, uuids, renderDistance }); + }; + + const startInterval = () => { + if (!intervalId) { + intervalId = setInterval(handleChange, 50); + } + }; + + const stopInterval = () => { + handleChange(); + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + }; + + controls.addEventListener('rest', handleChange); + controls.addEventListener('rest', stopInterval); + controls.addEventListener('control', startInterval); + controls.addEventListener('controlend', stopInterval); + + return () => { + controls.removeEventListener('rest', handleChange); + controls.removeEventListener('rest', stopInterval); + controls.removeEventListener('control', startInterval); + controls.removeEventListener('controlend', stopInterval); + if (intervalId) { + clearInterval(intervalId); + } + }; + } + }, [state.controls, floorItems, toggleView, renderDistance]); + + useEffect(() => { + const canvasElement = state.gl.domElement; + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + const onMouseUp = async (evt: any) => { + if (controls) { + (controls as any).enabled = true; + } + if (evt.button === 0) { + isLeftMouseDown = false; + if (drag) return; + + if (deleteModels) { + DeleteFloorItems(itemsGroup, hoveredDeletableFloorItem, setFloorItems, socket); + } + const Mode = transformMode; + + if (Mode !== null || activeTool === "Cursor") { + if (!itemsGroup.current) return; + let intersects = raycaster.intersectObjects(itemsGroup.current.children, true); + if (intersects.length > 0 && intersects[0]?.object?.parent?.parent?.position && intersects[0]?.object?.parent?.parent?.scale && intersects[0]?.object?.parent?.parent?.rotation) { + // let currentObject = intersects[0].object; + + // while (currentObject) { + // if (currentObject.name === "Scene") { + // break; + // } + // currentObject = currentObject.parent as THREE.Object3D; + // } + // if (currentObject) { + // AttachedObject.current = currentObject as any; + // setselectedFloorItem(AttachedObject.current!); + // } + } else { + const target = controls.getTarget(new THREE.Vector3()); + await controls.setTarget(target.x, 0, target.z, true); + setselectedFloorItem(null); + } + } + } + }; + + const onDblClick = async (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + if (drag) return; + + const Mode = transformMode; + + if (Mode !== null || activeTool === "Cursor") { + if (!itemsGroup.current) return; + let intersects = raycaster.intersectObjects(itemsGroup.current.children, true); + if (intersects.length > 0 && intersects[0]?.object?.parent?.parent?.position && intersects[0]?.object?.parent?.parent?.scale && intersects[0]?.object?.parent?.parent?.rotation) { + let currentObject = intersects[0].object; + + while (currentObject) { + if (currentObject.name === "Scene") { + break; + } + currentObject = currentObject.parent as THREE.Object3D; + } + if (currentObject) { + AttachedObject.current = currentObject as any; + // controls.fitToSphere(AttachedObject.current!, true); + + const bbox = new THREE.Box3().setFromObject(AttachedObject.current); + const size = bbox.getSize(new THREE.Vector3()); + const center = bbox.getCenter(new THREE.Vector3()); + + const front = new THREE.Vector3(0, 0, 1); + AttachedObject.current.localToWorld(front); + front.sub(AttachedObject.current.position).normalize(); + + const distance = Math.max(size.x, size.y, size.z) * 2; + const newPosition = center.clone().addScaledVector(front, distance); + + controls.setPosition(newPosition.x, newPosition.y, newPosition.z, true); + controls.setTarget(center.x, center.y, center.z, true); + controls.fitToBox(AttachedObject.current!, true, { cover: true, paddingTop: 5, paddingLeft: 5, paddingBottom: 5, paddingRight: 5 }); + + setselectedFloorItem(AttachedObject.current!); + } + } else { + const target = controls.getTarget(new THREE.Vector3()); + await controls.setTarget(target.x, 0, target.z, true); + setselectedFloorItem(null); + } + } + } + } + + const onDrop = (event: any) => { + + if (!event.dataTransfer?.files[0]) return; + + if (selectedItem.id !== "" && event.dataTransfer?.files[0]) { + addAssetModel(raycaster, state.camera, state.pointer, floorGroup, setFloorItems, itemsGroup, isTempLoader, tempLoader, socket, selectedItem, setSelectedItem, plane); + } + } + + const onDragOver = (event: any) => { + event.preventDefault(); + }; + + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("dblclick", onDblClick); + canvasElement.addEventListener("drop", onDrop); + canvasElement.addEventListener("dragover", onDragOver); + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("dblclick", onDblClick); + canvasElement.removeEventListener("drop", onDrop); + canvasElement.removeEventListener("dragover", onDragOver); + }; + }, [deleteModels, transformMode, controls, selectedItem, state.camera, state.pointer, activeTool]); + + useFrame(() => { + if (controls) + assetVisibility(itemsGroup, state.camera.position, renderDistance); + if (deleteModels) { + DeletableHoveredFloorItems(state, itemsGroup, hoveredDeletableFloorItem, setDeletableFloorItem); + } else if (!deleteModels) { + if (hoveredDeletableFloorItem.current) { + hoveredDeletableFloorItem.current = undefined; + setDeletableFloorItem(null); + } + } + }) + + return ( + + + ) +} + +export default FloorItemsGroup; \ No newline at end of file diff --git a/app/src/modules/builder/groups/floorPlanGroup.tsx b/app/src/modules/builder/groups/floorPlanGroup.tsx new file mode 100644 index 0000000..a767f34 --- /dev/null +++ b/app/src/modules/builder/groups/floorPlanGroup.tsx @@ -0,0 +1,197 @@ +import { useEffect } from "react"; +import * as Types from '../../../types/world/worldTypes'; +import { useActiveLayer, useDeletedLines, useDeletePointOrLine, useToolMode, useMovePoint, useNewLines, useRemovedLayer, useSocketStore, useToggleView, useUpdateScene } from "../../../store/store"; +import Layer2DVisibility from "../geomentries/layers/layer2DVisibility"; +import { useFrame, useThree } from "@react-three/fiber"; +import DeletableLineorPoint from "../functions/deletableLineOrPoint"; +import removeSoloPoint from "../geomentries/points/removeSoloPoint"; +import removeReferenceLine from "../geomentries/lines/removeReferenceLine"; +import DeleteLayer from "../geomentries/layers/deleteLayer"; +import { getLines } from "../../../services/factoryBuilder/lines/getLinesApi"; +import objectLinesToArray from "../geomentries/lines/lineConvertions/objectLinesToArray"; +import loadInitialPoint from "../../scene/IntialLoad/loadInitialPoint"; +import loadInitialLine from "../../scene/IntialLoad/loadInitialLine"; +import deletePoint from "../geomentries/points/deletePoint"; +import deleteLine from "../geomentries/lines/deleteLine"; +import drawWall from "../geomentries/lines/drawWall"; +import drawOnlyFloor from "../geomentries/floors/drawOnlyFloor"; +import addDragControl from "../eventDeclaration/dragControlDeclaration"; + + +const FloorPlanGroup = ({ floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoint, floorGroup, currentLayerPoint, dragPointControls, hoveredDeletablePoint, hoveredDeletableLine, plane, line, lines, onlyFloorline, onlyFloorlines, ReferenceLineMesh, LineCreated, isSnapped, ispreSnapped, snappedPoint, isSnappedUUID, isAngleSnapped, anglesnappedPoint }: any) => { + const state = useThree(); + const { scene, camera, gl, raycaster, controls } = state; + const { activeLayer, setActiveLayer } = useActiveLayer(); + const { toggleView, setToggleView } = useToggleView(); + const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); + const { toolMode, setToolMode } = useToolMode(); + const { movePoint, setMovePoint } = useMovePoint(); + const { removedLayer, setRemovedLayer } = useRemovedLayer(); + const { updateScene, setUpdateScene } = useUpdateScene(); + const { newLines, setNewLines } = useNewLines(); + const { deletedLines, setDeletedLines } = useDeletedLines(); + const { socket } = useSocketStore(); + + useEffect(() => { + addDragControl(dragPointControls, currentLayerPoint, state, floorPlanGroupPoint, floorPlanGroupLine, lines, onlyFloorlines, socket); + }, [state]); + + useEffect(() => { + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + // Load data from localStorage if available + getLines(organization).then((data) => { + + const Lines: Types.Lines = objectLinesToArray(data); + + // const data = localStorage.getItem("Lines"); + + if (Lines) { + lines.current = Lines; + loadInitialPoint(lines, floorPlanGroupPoint, currentLayerPoint, dragPointControls); + loadInitialLine(floorPlanGroupLine, lines); + setUpdateScene(true); + } + }) + }, []); + + useEffect(() => { + if (!toggleView) { + removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint); + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + }, [toggleView]); + + useEffect(() => { + if (toolMode === "Wall" || toolMode === "Floor") { + setDeletePointOrLine(false); + setMovePoint(false); + } else { + removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint); + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + }, [toolMode]); + + useEffect(() => { + if (movePoint) { + setToolMode(null); + setDeletePointOrLine(false); + if (dragPointControls.current) { + dragPointControls.current.enabled = true; + } + } else { + if (dragPointControls.current) { + dragPointControls.current.enabled = false; + } + } + }, [movePoint, toolMode]); + + useEffect(() => { + if (deletePointOrLine) { + setToolMode(null); + setMovePoint(false); + } + }, [deletePointOrLine]); + + useEffect(() => { + Layer2DVisibility(activeLayer, floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoint, currentLayerPoint, dragPointControls); + }, [activeLayer]); + + useEffect(() => { + if (removedLayer !== null) { + DeleteLayer(removedLayer, lines, floorPlanGroupLine, floorPlanGroupPoint, onlyFloorlines, floorGroup, setDeletedLines, setRemovedLayer, socket); + } + }, [removedLayer]); + + useEffect(() => { + + const canvasElement = gl.domElement; + + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; + + const onMouseUp = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + } + if (controls) { + (controls as any).enabled = true; + } + } + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + const onContextMenu = (e: any) => { + e.preventDefault(); + if (toolMode === "Wall" || toolMode === "Floor") { + removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint); + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + }; + + const onMouseClick = (evt: any) => { + if (!plane.current || drag) return; + + if (deletePointOrLine) { + if (hoveredDeletablePoint.current !== null) { + deletePoint(hoveredDeletablePoint, onlyFloorlines, floorPlanGroupPoint, floorPlanGroupLine, lines, setDeletedLines, socket); + } + if (hoveredDeletableLine.current !== null) { + deleteLine(hoveredDeletableLine, onlyFloorlines, lines, floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, socket); + } + } + + if (toolMode === "Wall") { + drawWall(raycaster, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket); + } + + if (toolMode === "Floor") { + drawOnlyFloor(raycaster, state, camera, plane, floorPlanGroupPoint, snappedPoint, isSnapped, isSnappedUUID, line, ispreSnapped, anglesnappedPoint, isAngleSnapped, onlyFloorline, onlyFloorlines, lines, floorPlanGroupLine, floorPlanGroup, ReferenceLineMesh, LineCreated, currentLayerPoint, dragPointControls, setNewLines, setDeletedLines, activeLayer, socket); + } + } + + if (deletePointOrLine || toolMode === "Wall" || toolMode === "Floor") { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("click", onMouseClick); + canvasElement.addEventListener("contextmenu", onContextMenu); + } + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("click", onMouseClick); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [deletePointOrLine, toolMode, activeLayer]) + + + useFrame(() => { + if (deletePointOrLine) { + DeletableLineorPoint(state, plane, floorPlanGroupLine, floorPlanGroupPoint, hoveredDeletableLine, hoveredDeletablePoint); + } + }) + + return ( + + + + + ) +} + +export default FloorPlanGroup; \ No newline at end of file diff --git a/app/src/modules/builder/groups/wallItemsGroup.tsx b/app/src/modules/builder/groups/wallItemsGroup.tsx new file mode 100644 index 0000000..a8c6211 --- /dev/null +++ b/app/src/modules/builder/groups/wallItemsGroup.tsx @@ -0,0 +1,289 @@ +import { useEffect } from "react"; +import { useDeleteModels, useDeletePointOrLine, useObjectPosition, useObjectRotation, useObjectScale, useSelectedWallItem, useSocketStore, useWallItems } from "../../../store/store"; +import { Csg } from "../csg/csg"; +import * as Types from "../../../types/world/worldTypes"; +import * as CONSTANTS from "../../../types/world/worldConstants"; +import * as THREE from "three"; +import { useThree } from "@react-three/fiber"; +import handleMeshMissed from "../eventFunctions/handleMeshMissed"; +import DeleteWallItems from "../geomentries/walls/deleteWallItems"; +import loadInitialWallItems from "../../scene/IntialLoad/loadInitialWallItems"; +import AddWallItems from "../geomentries/walls/addWallItems"; + + +const WallItemsGroup = ({ currentWallItem, AssetConfigurations, hoveredDeletableWallItem, selectedItemsIndex, setSelectedItemsIndex, CSGGroup }: any) => { + const { deleteModels, setDeleteModels } = useDeleteModels(); + const { wallItems, setWallItems } = useWallItems(); + const { objectPosition, setObjectPosition } = useObjectPosition(); + const { objectScale, setObjectScale } = useObjectScale(); + const { objectRotation, setObjectRotation } = useObjectRotation(); + const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); + const { selectedWallItem, setSelectedWallItem } = useSelectedWallItem(); + const { socket } = useSocketStore(); + const state = useThree(); + const { pointer, camera, raycaster } = state; + + + useEffect(() => { + // Load Wall Items from the backend + loadInitialWallItems(setWallItems, AssetConfigurations); + }, []); + + + ////////// Update the Scale value changes in thewallItems State ////////// + + ////////// Update the Position value changes in the selected item ////////// + + ////////// Update the Rotation value changes in the selected item ////////// + + useEffect(() => { + if (objectScale.x && objectScale.y && objectScale.z) { + let ScaledWallItems: Types.wallItems = []; + wallItems.forEach((items: any) => { + if (items.model?.uuid === currentWallItem.current?.parent?.uuid) { + items.scale = [objectScale.x, objectScale.y, objectScale.z]; + } + ScaledWallItems.push(items); + }); + setWallItems(ScaledWallItems); + } + }, [objectScale]); + + useEffect(() => { + if (objectPosition.x && objectPosition.y && objectPosition.z) { + let ScaledWallItems: Types.wallItems = []; + wallItems.forEach((items: any) => { + if (items.model?.uuid === currentWallItem.current?.parent?.uuid) { + items.position = [objectPosition.x, objectPosition.y, objectPosition.z]; + } + ScaledWallItems.push(items); + }); + setWallItems(ScaledWallItems); + } + }, [objectPosition]); + + useEffect(() => { + if (objectRotation.x && objectRotation.y && objectRotation.z) { + let ScaledWallItems: Types.wallItems = []; + wallItems.forEach((items: any) => { + if (items.model?.uuid === currentWallItem.current?.parent?.uuid) { + const radiansX = objectRotation.x * (Math.PI / 180); + const radiansY = objectRotation.y * (Math.PI / 180); + const radiansZ = objectRotation.z * (Math.PI / 180); + const quaternion = new THREE.Quaternion().setFromEuler( + new THREE.Euler(radiansX, radiansY, radiansZ) + ); + items.quaternion = [quaternion.x, quaternion.y, quaternion.z, quaternion.w]; + } + ScaledWallItems.push(items); + }); + setWallItems(ScaledWallItems); + } + }, [objectRotation]); + + useEffect(() => { + const canvasElement = state.gl.domElement; + function handlePointerMove(e: any) { + if (selectedItemsIndex !== null && !deletePointOrLine && e.buttons === 1) { + const Raycaster = state.raycaster; + const intersects = Raycaster.intersectObjects(CSGGroup.current?.children[0].children!, true); + const Object = intersects.find((child) => child.object.name.includes("WallRaycastReference")); + + if (Object) { + (state.controls as any)!.enabled = false; + setWallItems((prevItems: any) => { + const updatedItems = [...prevItems]; + let position: [number, number, number] = [0, 0, 0]; + + if (updatedItems[selectedItemsIndex].type === "Fixed-Move") { + position = [Object!.point.x, Math.floor(Object!.point.y / CONSTANTS.wallConfig.height) * CONSTANTS.wallConfig.height, Object!.point.z]; + } else if (updatedItems[selectedItemsIndex].type === "Free-Move") { + position = [Object!.point.x, Object!.point.y, Object!.point.z]; + } + + requestAnimationFrame(() => { + setObjectPosition(new THREE.Vector3(...position)); + setObjectRotation({ + x: THREE.MathUtils.radToDeg(Object!.object.rotation.x), + y: THREE.MathUtils.radToDeg(Object!.object.rotation.y), + z: THREE.MathUtils.radToDeg(Object!.object.rotation.z), + }); + }); + + updatedItems[selectedItemsIndex] = { + ...updatedItems[selectedItemsIndex], + position: position, + quaternion: Object!.object.quaternion.clone() as Types.QuaternionType, + }; + + return updatedItems; + }); + } + } + } + + async function handlePointerUp() { + const Raycaster = state.raycaster; + const intersects = Raycaster.intersectObjects(CSGGroup.current?.children[0].children!, true); + const Object = intersects.find((child) => child.object.name.includes("WallRaycastReference")); + if (Object) { + if (selectedItemsIndex !== null) { + let currentItem: any = null; + setWallItems((prevItems: any) => { + const updatedItems = [...prevItems]; + const WallItemsForStorage = updatedItems.map((item) => { + const { model, ...rest } = item; + return { + ...rest, + modeluuid: model?.uuid, + }; + }); + + currentItem = updatedItems[selectedItemsIndex]; + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + return updatedItems; + }); + + setTimeout(async () => { + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // await setWallItem( + // organization, + // currentItem?.model?.uuid, + // currentItem.modelname, + // currentItem.type!, + // currentItem.csgposition!, + // currentItem.csgscale!, + // currentItem.position, + // currentItem.quaternion, + // currentItem.scale!, + // ) + + //SOCKET + + const data = { + organization: organization, + modeluuid: currentItem.model?.uuid!, + modelname: currentItem.modelname!, + type: currentItem.type!, + csgposition: currentItem.csgposition!, + csgscale: currentItem.csgscale!, + position: currentItem.position!, + quaternion: currentItem.quaternion, + scale: currentItem.scale!, + socketId: socket.id + } + + socket.emit('v1:wallItems:set', data); + }, 0); + (state.controls as any)!.enabled = true; + } + } + } + + canvasElement.addEventListener("pointermove", handlePointerMove); + canvasElement.addEventListener("pointerup", handlePointerUp); + + return () => { + canvasElement.removeEventListener("pointermove", handlePointerMove); + canvasElement.removeEventListener("pointerup", handlePointerUp); + }; + }, [selectedItemsIndex]); + + useEffect(() => { + const canvasElement = state.gl.domElement; + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; + + const onMouseUp = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + if (!drag && deleteModels) { + DeleteWallItems(hoveredDeletableWallItem, setWallItems, wallItems, socket); + } + } + }; + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + const onDrop = (event: any) => { + + if (!event.dataTransfer?.files[0]) return + pointer.x = (event.clientX / window.innerWidth) * 2 - 1; + pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; + raycaster.setFromCamera(pointer, camera); + + if (AssetConfigurations[(event.dataTransfer.files[0].name.split('.'))[0]]) { + const selected = (event.dataTransfer.files[0].name.split('.'))[0]; + + if (AssetConfigurations[selected]?.type) { + AddWallItems(selected, raycaster, CSGGroup, AssetConfigurations, setWallItems, socket); + } + event.preventDefault(); + } + } + + const onDragOver = (event: any) => { + event.preventDefault(); + }; + + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("drop", onDrop); + canvasElement.addEventListener("dragover", onDragOver); + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("drop", onDrop); + canvasElement.removeEventListener("dragover", onDragOver); + }; + }, [deleteModels, wallItems]) + + useEffect(() => { + if (deleteModels) { + handleMeshMissed(currentWallItem, setSelectedWallItem, setSelectedItemsIndex); + setSelectedWallItem(null); + setSelectedItemsIndex(null); + } + }, [deleteModels]) + + return ( + <> + {wallItems.map((item: Types.WallItem, index: number) => ( + + + + ))} + + ) +} + +export default WallItemsGroup; \ No newline at end of file diff --git a/app/src/modules/builder/groups/wallsAndWallItems.tsx b/app/src/modules/builder/groups/wallsAndWallItems.tsx new file mode 100644 index 0000000..a4e7d71 --- /dev/null +++ b/app/src/modules/builder/groups/wallsAndWallItems.tsx @@ -0,0 +1,56 @@ +import { Geometry } from "@react-three/csg"; +import { useDeleteModels, useSelectedWallItem, useToggleView, useTransformMode, useWallItems, useWalls } from "../../../store/store"; +import handleMeshDown from "../eventFunctions/handleMeshDown"; +import handleMeshMissed from "../eventFunctions/handleMeshMissed"; +import WallsMesh from "./wallsMesh"; +import WallItemsGroup from "./wallItemsGroup"; +import { useEffect } from "react"; + + +const WallsAndWallItems = ({ CSGGroup, AssetConfigurations, setSelectedItemsIndex, selectedItemsIndex, currentWallItem, csg, lines, hoveredDeletableWallItem }: any) => { + const { walls, setWalls } = useWalls(); + const { wallItems, setWallItems } = useWallItems(); + const { toggleView, setToggleView } = useToggleView(); + const { deleteModels, setDeleteModels } = useDeleteModels(); + const { transformMode, setTransformMode } = useTransformMode(); + const { selectedWallItem, setSelectedWallItem } = useSelectedWallItem(); + + useEffect(() => { + if (transformMode === null) { + if (!deleteModels) { + handleMeshMissed(currentWallItem, setSelectedWallItem, setSelectedItemsIndex); + setSelectedWallItem(null); + setSelectedItemsIndex(null); + } + } + }, [transformMode]) + + return ( + { + if (!deleteModels && transformMode !== null) { + handleMeshDown(event, currentWallItem, setSelectedWallItem, setSelectedItemsIndex, wallItems, toggleView); + } + }} + onPointerMissed={() => { + if (!deleteModels) { + handleMeshMissed(currentWallItem, setSelectedWallItem, setSelectedItemsIndex); + setSelectedWallItem(null); + setSelectedItemsIndex(null); + } + }} + > + + + + + + ) +} + +export default WallsAndWallItems; \ No newline at end of file diff --git a/app/src/modules/builder/groups/wallsMesh.tsx b/app/src/modules/builder/groups/wallsMesh.tsx new file mode 100644 index 0000000..a82bbb4 --- /dev/null +++ b/app/src/modules/builder/groups/wallsMesh.tsx @@ -0,0 +1,65 @@ +import * as THREE from 'three'; +import * as Types from '../../../types/world/worldTypes'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import { Base } from '@react-three/csg'; +import { MeshDiscardMaterial } from '@react-three/drei'; +import { useUpdateScene, useWalls } from '../../../store/store'; +import { useEffect } from 'react'; +import { getLines } from '../../../services/factoryBuilder/lines/getLinesApi'; +import objectLinesToArray from '../geomentries/lines/lineConvertions/objectLinesToArray'; +import loadWalls from '../geomentries/walls/loadWalls'; + +const WallsMesh = ({ lines }: any) => { + const { walls, setWalls } = useWalls(); + const { updateScene, setUpdateScene } = useUpdateScene(); + + useEffect(() => { + if (updateScene) { + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + getLines(organization).then((data) => { + const Lines: Types.Lines = objectLinesToArray(data); + localStorage.setItem("Lines", JSON.stringify(Lines)); + + if (Lines) { + loadWalls(lines, setWalls); + } + }) + setUpdateScene(false); + } + }, [updateScene]) + + return ( + <> + {walls.map((wall: Types.Wall, index: number) => ( + + + + + + + + + ))} + + ) +} + +export default WallsMesh; \ No newline at end of file diff --git a/app/src/modules/builder/groups/zoneGroup.tsx b/app/src/modules/builder/groups/zoneGroup.tsx new file mode 100644 index 0000000..bd0087e --- /dev/null +++ b/app/src/modules/builder/groups/zoneGroup.tsx @@ -0,0 +1,466 @@ +import React, { useState, useEffect, useMemo, useRef } from "react"; +import { Line, Sphere } from "@react-three/drei"; +import { useThree, useFrame } from "@react-three/fiber"; +import * as THREE from "three"; +import { useActiveLayer, useDeleteModels, useDeletePointOrLine, useMovePoint, useSocketStore, useToggleView, useToolMode, useRemovedLayer, useZones, useZonePoints } from "../../../store/store"; +// import { setZonesApi } from "../../../services/factoryBuilder/zones/setZonesApi"; +// import { deleteZonesApi } from "../../../services/factoryBuilder/zones/deleteZoneApi"; +import { getZonesApi } from "../../../services/factoryBuilder/zones/getZonesApi"; + +import * as CONSTANTS from '../../../types/world/worldConstants'; + +const ZoneGroup: React.FC = () => { + const { camera, pointer, gl, raycaster, scene, controls } = useThree(); + const [startPoint, setStartPoint] = useState(null); + const [endPoint, setEndPoint] = useState(null); + const { zones, setZones } = useZones(); + const { zonePoints, setZonePoints } = useZonePoints(); + const [isDragging, setIsDragging] = useState(false); + const [draggedSphere, setDraggedSphere] = useState(null); + const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); + const { toggleView } = useToggleView(); + const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); + const { removedLayer, setRemovedLayer } = useRemovedLayer(); + const { toolMode, setToolMode } = useToolMode(); + const { movePoint, setMovePoint } = useMovePoint(); + const { deleteModels, setDeleteModels } = useDeleteModels(); + const { activeLayer, setActiveLayer } = useActiveLayer(); + const { socket } = useSocketStore(); + + const groupsRef = useRef(); + + const zoneMaterial = useMemo(() => new THREE.ShaderMaterial({ + side: THREE.DoubleSide, + vertexShader: ` + varying vec2 vUv; + void main(){ + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + vUv = uv; + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform vec3 uColor; + void main(){ + float alpha = 1.0 - vUv.y; + gl_FragColor = vec4(uColor, alpha); + } + `, + uniforms: { + uColor: { value: new THREE.Color(CONSTANTS.zoneConfig.color) }, + }, + transparent: true, + }), []); + + useEffect(() => { + const fetchZones = async () => { + const email = localStorage.getItem('email'); + if (!email) return; + + const organization = email.split("@")[1].split(".")[0]; + const data = await getZonesApi(organization); + + if (data.data && data.data.length > 0) { + const fetchedZones = data.data.map((zone: any) => ({ + zoneId: zone.zoneId, + zoneName: zone.zoneName, + points: zone.points, + layer: zone.layer + })); + + setZones(fetchedZones); + + const fetchedPoints = data.data.flatMap((zone: any) => + zone.points.slice(0, 4).map((point: [number, number, number]) => new THREE.Vector3(...point)) + ); + + setZonePoints(fetchedPoints); + } + }; + + fetchZones(); + }, []); + + useEffect(() => { + + localStorage.setItem('zones', zones); + + }, [zones]) + + useEffect(() => { + if (removedLayer) { + const updatedZones = zones.filter((zone: any) => zone.layer !== removedLayer); + setZones(updatedZones); + + const updatedzonePoints = zonePoints.filter((_: any, index: any) => { + const zoneIndex = Math.floor(index / 4); + return zones[zoneIndex]?.layer !== removedLayer; + }); + setZonePoints(updatedzonePoints); + + zones.filter((zone: any) => zone.layer === removedLayer).forEach((zone: any) => { + deleteZoneFromBackend(zone.zoneId); + }); + + setRemovedLayer(null); + } + }, [removedLayer]); + + useEffect(() => { + if (toolMode !== "Zone") { + setStartPoint(null); + setEndPoint(null); + } else { + setDeletePointOrLine(false); + setMovePoint(false); + setDeleteModels(false); + } + if (!toggleView) { + setStartPoint(null); + setEndPoint(null); + } + }, [toolMode, toggleView]); + + + const addZoneToBackend = async (zone: { zoneId: string; zoneName: string; points: [number, number, number][]; layer: string }) => { + + const email = localStorage.getItem('email'); + const userId = localStorage.getItem('userId'); + const organization = (email!.split("@")[1]).split(".")[0]; + + const input = { + userId: userId, + organization: organization, + zoneData: { + zoneName: zone.zoneName, + zoneId: zone.zoneId, + points: zone.points, + layer: zone.layer + } + } + + socket.emit('v2:zone:set', input); + }; + + const updateZoneToBackend = async (zone: { zoneId: string; zoneName: string; points: [number, number, number][]; layer: string }) => { + + const email = localStorage.getItem('email'); + const userId = localStorage.getItem('userId'); + const organization = (email!.split("@")[1]).split(".")[0]; + + const input = { + userId: userId, + organization: organization, + zoneData: { + zoneName: zone.zoneName, + zoneId: zone.zoneId, + points: zone.points, + layer: zone.layer + } + } + + socket.emit('v2:zone:set', input); + }; + + const deleteZoneFromBackend = async (zoneId: string) => { + + const email = localStorage.getItem('email'); + const userId = localStorage.getItem('userId'); + const organization = (email!.split("@")[1]).split(".")[0]; + + const input = { + userId: userId, + organization: organization, + zoneId: zoneId + } + + socket.emit('v2:zone:delete', input); + }; + + const handleDeleteZone = (zoneId: string) => { + const updatedZones = zones.filter((zone: any) => zone.zoneId !== zoneId); + setZones(updatedZones); + + const zoneIndex = zones.findIndex((zone: any) => zone.zoneId === zoneId); + if (zoneIndex !== -1) { + const zonePointsToRemove = zonePoints.slice(zoneIndex * 4, zoneIndex * 4 + 4); + zonePointsToRemove.forEach((point: any) => groupsRef.current.remove(point)); + const updatedzonePoints = zonePoints.filter((_: any, index: any) => index < zoneIndex * 4 || index >= zoneIndex * 4 + 4); + setZonePoints(updatedzonePoints); + } + + deleteZoneFromBackend(zoneId); + }; + + useEffect(() => { + if (!camera || !toggleView) return; + const canvasElement = gl.domElement; + + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(groupsRef.current.children, true); + + if (intersects.length > 0 && movePoint) { + const clickedObject = intersects[0].object; + const sphereIndex = zonePoints.findIndex((point: any) => point.equals(clickedObject.position)); + if (sphereIndex !== -1) { + (controls as any).enabled = false; + setDraggedSphere(zonePoints[sphereIndex]); + setIsDragging(true); + } + } + } + }; + + const onMouseUp = (evt: any) => { + if (evt.button === 0 && !drag && !isDragging && !deletePointOrLine) { + isLeftMouseDown = false; + + if (!startPoint && !movePoint) { + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (point) { + setStartPoint(point); + setEndPoint(null); + } + } else if (startPoint && !movePoint) { + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (!point) return; + + const points = [ + [startPoint.x, 0.15, startPoint.z], + [point.x, 0.15, startPoint.z], + [point.x, 0.15, point.z], + [startPoint.x, 0.15, point.z], + [startPoint.x, 0.15, startPoint.z], + ] as [number, number, number][]; + + const zoneName = `Zone ${zones.length + 1}`; + const zoneId = THREE.MathUtils.generateUUID(); + const newZone = { + zoneId, + zoneName, + points: points, + layer: activeLayer + }; + + const newZones = [...zones, newZone]; + + setZones(newZones); + + const newzonePoints = [ + new THREE.Vector3(startPoint.x, 0.15, startPoint.z), + new THREE.Vector3(point.x, 0.15, startPoint.z), + new THREE.Vector3(point.x, 0.15, point.z), + new THREE.Vector3(startPoint.x, 0.15, point.z), + ]; + + const updatedZonePoints = [...zonePoints, ...newzonePoints]; + setZonePoints(updatedZonePoints); + + addZoneToBackend(newZone); + + setStartPoint(null); + setEndPoint(null); + } + } else if (evt.button === 0 && !drag && !isDragging && deletePointOrLine) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(groupsRef.current.children, true); + + if (intersects.length > 0) { + const clickedObject = intersects[0].object; + + const sphereIndex = zonePoints.findIndex((point: any) => point.equals(clickedObject.position)); + if (sphereIndex !== -1) { + const zoneIndex = Math.floor(sphereIndex / 4); + const zoneId = zones[zoneIndex].zoneId; + handleDeleteZone(zoneId); + return; + } + } + } + + if (evt.button === 0) { + if (isDragging && draggedSphere) { + setIsDragging(false); + setDraggedSphere(null); + + const sphereIndex = zonePoints.findIndex((point: any) => point === draggedSphere); + if (sphereIndex !== -1) { + const zoneIndex = Math.floor(sphereIndex / 4); + + if (zoneIndex !== -1 && zones[zoneIndex]) { + updateZoneToBackend(zones[zoneIndex]); + } + } + } + } + }; + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(groupsRef.current.children, true); + + if (intersects.length > 0 && intersects[0].object.name.includes('point')) { + gl.domElement.style.cursor = movePoint ? "pointer" : "default"; + } else { + gl.domElement.style.cursor = "default"; + } + if (isDragging && draggedSphere) { + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (point) { + draggedSphere.set(point.x, 0.15, point.z); + + const sphereIndex = zonePoints.findIndex((point: any) => point === draggedSphere); + if (sphereIndex !== -1) { + const zoneIndex = Math.floor(sphereIndex / 4); + const cornerIndex = sphereIndex % 4; + + const updatedZones = zones.map((zone: any, index: number) => { + if (index === zoneIndex) { + const updatedPoints = [...zone.points]; + updatedPoints[cornerIndex] = [point.x, 0.15, point.z]; + updatedPoints[4] = updatedPoints[0]; + return { ...zone, points: updatedPoints }; + } + return zone; + }); + + setZones(updatedZones); + } + } + } + }; + + const onContext = (event: any) => { + event.preventDefault(); + setStartPoint(null); + setEndPoint(null); + }; + + if (toolMode === 'Zone' || deletePointOrLine || movePoint) { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("contextmenu", onContext); + } + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("contextmenu", onContext); + }; + }, [gl, camera, startPoint, toggleView, scene, toolMode, zones, isDragging, deletePointOrLine, zonePoints, draggedSphere, movePoint, activeLayer]); + + useFrame(() => { + if (!startPoint) return; + raycaster.setFromCamera(pointer, camera); + const intersectionPoint = new THREE.Vector3(); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (point) { + setEndPoint(point); + } + }); + return ( + + + {zones + .map((zone: any) => ( + + {zone.points.slice(0, -1).map((point: [number, number, number], index: number) => { + const nextPoint = zone.points[index + 1]; + + const point1 = new THREE.Vector3(point[0], point[1], point[2]); + const point2 = new THREE.Vector3(nextPoint[0], nextPoint[1], nextPoint[2]); + + const planeWidth = point1.distanceTo(point2); + const planeHeight = CONSTANTS.wallConfig.height; + + const midpoint = new THREE.Vector3((point1.x + point2.x) / 2, (CONSTANTS.wallConfig.height / 2) + ((zone.layer - 1) * CONSTANTS.wallConfig.height), (point1.z + point2.z) / 2); + + const angle = Math.atan2(point2.z - point1.z, point2.x - point1.x); + + return ( + + + + + ); + })} + + ))} + + + {zones + .filter((zone: any) => zone.layer === activeLayer) + .map((zone: any) => ( + { + e.stopPropagation(); + if (deletePointOrLine) { + handleDeleteZone(zone.zoneId); + } + }} + /> + ))} + + + {zones.filter((zone: any) => zone.layer === activeLayer).flatMap((zone: any) => ( + zone.points.slice(0, 4).map((point: any, pointIndex: number) => ( + + + + )) + ))} + + + {startPoint && endPoint && ( + + )} + + + ); +}; + +export default ZoneGroup; \ No newline at end of file diff --git a/app/src/modules/builder/groups/zoneGroup1.tsx b/app/src/modules/builder/groups/zoneGroup1.tsx new file mode 100644 index 0000000..7104bb0 --- /dev/null +++ b/app/src/modules/builder/groups/zoneGroup1.tsx @@ -0,0 +1,245 @@ +import { useEffect } from "react"; +import * as THREE from 'three'; +import * as Types from '../../../types/world/worldTypes'; +import * as CONSTANTS from "../../../types/world/worldConstants"; +import { useActiveLayer, useSocketStore, useDeleteModels, useDeletePointOrLine, useMovePoint, useToggleView, useUpdateScene, useNewLines, useToolMode } from "../../../store/store"; +import { useThree } from "@react-three/fiber"; +import arrayLineToObject from "../geomentries/lines/lineConvertions/arrayLineToObject"; +import addPointToScene from "../geomentries/points/addPointToScene"; +import addLineToScene from "../geomentries/lines/addLineToScene"; +import removeSoloPoint from "../geomentries/points/removeSoloPoint"; +import removeReferenceLine from "../geomentries/lines/removeReferenceLine"; +import getClosestIntersection from "../geomentries/lines/getClosestIntersection"; +import loadZones from "../geomentries/zones/loadZones"; + +const ZoneGroup = ({ zoneGroup, plane, floorPlanGroupLine, floorPlanGroupPoint, line, lines, currentLayerPoint, dragPointControls, floorPlanGroup, ReferenceLineMesh, LineCreated, isSnapped, ispreSnapped, snappedPoint, isSnappedUUID, isAngleSnapped, anglesnappedPoint }: any) => { + const { toggleView, setToggleView } = useToggleView(); + const { deleteModels, setDeleteModels } = useDeleteModels(); + const { deletePointOrLine, setDeletePointOrLine } = useDeletePointOrLine(); + const { toolMode, setToolMode } = useToolMode(); + const { movePoint, setMovePoint } = useMovePoint(); + const { socket } = useSocketStore(); + const { activeLayer } = useActiveLayer(); + const { gl, raycaster, camera, pointer } = useThree(); + const { updateScene, setUpdateScene } = useUpdateScene(); + const { newLines, setNewLines } = useNewLines(); + + useEffect(() => { + if (updateScene) { + loadZones(lines, zoneGroup); + setUpdateScene(false); + } + }, [updateScene]) + + useEffect(() => { + if (toolMode === "Zone") { + setDeletePointOrLine(false); + setMovePoint(false); + setDeleteModels(false); + } else { + removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint); + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + }, [toolMode]) + + useEffect(() => { + + const canvasElement = gl.domElement; + + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; + + const onMouseUp = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + } + } + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + const onContextMenu = (e: any) => { + e.preventDefault(); + if (toolMode === "Zone") { + removeSoloPoint(line, floorPlanGroupLine, floorPlanGroupPoint); + removeReferenceLine(floorPlanGroup, ReferenceLineMesh, LineCreated, line); + } + }; + + const onMouseClick = (evt: any) => { + if (!plane.current || drag) return; + const intersects = raycaster.intersectObject(plane.current, true); + let intersectionPoint = intersects[0].point; + const points = floorPlanGroupPoint.current?.children ?? []; + const intersectsPoint = raycaster.intersectObjects(points, true).find(intersect => intersect.object.visible); + let intersectsLines: any = raycaster.intersectObjects(floorPlanGroupLine.current.children, true); + + if (intersectsLines.length > 0 && intersects && intersects.length > 0 && !intersectsPoint) { + const lineType = intersectsLines[0].object.userData.linePoints[0][3]; + if (lineType === CONSTANTS.lineConfig.zoneName) { + // console.log("intersected a zone line"); + + const ThroughPoint = (intersectsLines[0].object.geometry.parameters.path).getPoints(300); + let intersection = getClosestIntersection(ThroughPoint, intersectionPoint); + if (!intersection) return; + const point = addPointToScene(intersection, CONSTANTS.pointConfig.zoneOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, undefined, CONSTANTS.lineConfig.zoneName); + (line.current as Types.Line).push([new THREE.Vector3(intersection.x, 0.01, intersection.z), point.uuid, activeLayer, CONSTANTS.lineConfig.zoneName,]); + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + lines.current.push(line.current as Types.Line); + + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]); + + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.zoneColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + } + } + } else if (intersectsPoint && intersects && intersects.length > 0) { + if (intersectsPoint.object.userData.type === CONSTANTS.lineConfig.zoneName) { + // console.log("intersected a zone point"); + + intersectionPoint = intersectsPoint.object.position; + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), intersectsPoint.object.uuid, activeLayer, CONSTANTS.lineConfig.zoneName]); + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + lines.current.push(line.current as Types.Line); + + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]); + + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.zoneColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + ispreSnapped.current = false; + isSnapped.current = false; + } + } + } else if (intersects && intersects.length > 0) { + // console.log("intersected a empty area"); + + let uuid: string = ""; + if (isAngleSnapped.current && anglesnappedPoint.current && line.current.length > 0) { + intersectionPoint = anglesnappedPoint.current; + const point = addPointToScene(intersectionPoint, CONSTANTS.pointConfig.zoneOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, undefined, CONSTANTS.lineConfig.zoneName); + uuid = point.uuid; + } else if (isSnapped.current && snappedPoint.current && line.current.length > 0) { + intersectionPoint = snappedPoint.current; + uuid = isSnappedUUID.current!; + } else if (ispreSnapped.current && snappedPoint.current) { + intersectionPoint = snappedPoint.current; + uuid = isSnappedUUID.current!; + } else { + const point = addPointToScene(intersectionPoint, CONSTANTS.pointConfig.zoneOuterColor, currentLayerPoint, floorPlanGroupPoint, dragPointControls, undefined, CONSTANTS.lineConfig.zoneName); + uuid = point.uuid; + } + + (line.current as Types.Line).push([new THREE.Vector3(intersectionPoint.x, 0.01, intersectionPoint.z), uuid, activeLayer, CONSTANTS.lineConfig.zoneName]); + if (line.current.length >= 2 && line.current[0] && line.current[1]) { + lines.current.push(line.current as Types.Line); + + const data = arrayLineToObject(line.current as Types.Line); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setLine(organization, data.layer!, data.line!, data.type!); + + //SOCKET + + const input = { + organization: organization, + layer: data.layer, + line: data.line, + type: data.type, + socketId: socket.id + } + + socket.emit('v1:Line:create', input); + + setNewLines([line.current]); + + addLineToScene(line.current[0][0], line.current[1][0], CONSTANTS.lineConfig.zoneColor, line.current, floorPlanGroupLine); + let lastPoint = line.current[line.current.length - 1]; + line.current = [lastPoint]; + ispreSnapped.current = false; + isSnapped.current = false; + } + } + } + + if (toolMode === 'Zone') { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("click", onMouseClick); + canvasElement.addEventListener("contextmenu", onContextMenu); + } + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("click", onMouseClick); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [toolMode]) + + return ( + + + ) +} + +export default ZoneGroup; \ No newline at end of file diff --git a/app/src/modules/collaboration/collabCams.tsx b/app/src/modules/collaboration/collabCams.tsx new file mode 100644 index 0000000..900b3d0 --- /dev/null +++ b/app/src/modules/collaboration/collabCams.tsx @@ -0,0 +1,142 @@ +import * as THREE from 'three'; +import { useEffect, useRef, useState } from 'react'; +import { useFrame } from '@react-three/fiber'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import camModel from '../../assets/gltf-glb/camera face 2.gltf'; +import getActiveUsersData from '../../services/factoryBuilder/collab/getActiveUsers'; +import { useActiveUsers, useSocketStore } from '../../store/store'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import { useNavigate } from 'react-router-dom'; +import { Text, Html } from '@react-three/drei'; +import CollabUserIcon from './collabUserIcon'; +import image from '../../assets/image/userImage.png'; + + +const CamModelsGroup = () => { + let navigate = useNavigate(); + const groupRef = useRef(null); + const email = localStorage.getItem('email'); + const { activeUsers, setActiveUsers } = useActiveUsers(); + const { socket } = useSocketStore(); + const loader = new GLTFLoader(); + const dracoLoader = new DRACOLoader(); + const [cams, setCams] = useState([]); + const [models, setModels] = useState>({}); + + dracoLoader.setDecoderPath('three/examples/jsm/libs/draco/gltf/'); + loader.setDRACOLoader(dracoLoader); + + useEffect(() => { + if (!email) { + navigate('/'); + } + if (!socket) return; + const organization = email!.split('@')[1].split('.')[0]; + + socket.on('userConnectRespones', (data: any) => { + if (!groupRef.current) return; + if (data.data.userData.email === email) return + if (socket.id === data.socketId || organization !== data.organization) return; + + const model = groupRef.current.getObjectByProperty('uuid', data.data.userData._id); + if (model) { + groupRef.current.remove(model); + } + loader.load(camModel, (gltf) => { + const newModel = gltf.scene.clone(); + newModel.uuid = data.data.userData._id; + newModel.position.set(data.data.position.x, data.data.position.y, data.data.position.z); + newModel.rotation.set(data.data.rotation.x, data.data.rotation.y, data.data.rotation.z); + newModel.userData = data.data.userData; + setCams((prev) => [...prev, newModel]); + setActiveUsers([...activeUsers, data.data.userData]); + }); + }); + + socket.on('userDisConnectRespones', (data: any) => { + if (!groupRef.current) return; + if (socket.id === data.socketId || organization !== data.organization) return; + + setCams((prev) => prev.filter((cam) => cam.uuid !== data.data.userData._id)); + setActiveUsers(activeUsers.filter((user: any) => user._id !== data.data.userData._id)); + }); + + socket.on('cameraUpdateResponse', (data: any) => { + if (!groupRef.current || socket.id === data.socketId || organization !== data.organization) return; + + setModels((prev) => ({ + ...prev, + [data.data.userId]: { + targetPosition: new THREE.Vector3(data.data.position.x, data.data.position.y, data.data.position.z), + targetRotation: new THREE.Euler(data.data.rotation.x, data.data.rotation.y, data.data.rotation.z), + }, + })); + }); + + return () => { + socket.off('userConnectRespones'); + socket.off('userDisConnectRespones'); + socket.off('cameraUpdateResponse'); + }; + }, [socket]); + + useFrame(() => { + if (!groupRef.current) return; + Object.keys(models).forEach((uuid) => { + const model = groupRef.current!.getObjectByProperty('uuid', uuid); + if (!model) return; + + const { targetPosition, targetRotation } = models[uuid]; + model.position.lerp(targetPosition, 0.1); + model.rotation.x = THREE.MathUtils.lerp(model.rotation.x, targetRotation.x, 0.1); + model.rotation.y = THREE.MathUtils.lerp(model.rotation.y, targetRotation.y, 0.1); + model.rotation.z = THREE.MathUtils.lerp(model.rotation.z, targetRotation.z, 0.1); + }); + }); + + useEffect(() => { + if (!groupRef.current) return; + const organization = email!.split('@')[1].split('.')[0]; + getActiveUsersData(organization).then((data) => { + const filteredData = data.cameraDatas.filter((camera: any) => camera.userData.email !== email); + if (filteredData.length > 0) { + loader.load(camModel, (gltf) => { + const newCams = filteredData.map((cam: any) => { + const newModel = gltf.scene.clone(); + newModel.uuid = cam.userData._id; + newModel.position.set(cam.position.x, cam.position.y, cam.position.z); + newModel.rotation.set(cam.rotation.x, cam.rotation.y, cam.rotation.z); + newModel.userData = cam.userData; + setActiveUsers([...activeUsers, cam.userData]); + return newModel; + }); + setCams((prev) => [...prev, ...newCams]); + }); + } + }); + }, []); + + return ( + + {cams.map((cam, index) => ( + + + + + + ))} + + ); +}; + +export default CamModelsGroup; diff --git a/app/src/modules/collaboration/collabUserIcon.tsx b/app/src/modules/collaboration/collabUserIcon.tsx new file mode 100644 index 0000000..4113f6a --- /dev/null +++ b/app/src/modules/collaboration/collabUserIcon.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +interface CollabUserIconProps { + color: string; + userImage: string; + userName: string; +} + +const CollabUserIcon: React.FC = ({ + color, + userImage, + userName, +}) => { + return ( +
+ {userName} +
+ {userName} +
+
+ ); +}; + +export default CollabUserIcon; diff --git a/app/src/modules/collaboration/socketResponses.dev.tsx b/app/src/modules/collaboration/socketResponses.dev.tsx new file mode 100644 index 0000000..b8dfc2c --- /dev/null +++ b/app/src/modules/collaboration/socketResponses.dev.tsx @@ -0,0 +1,777 @@ +import { useEffect } from "react"; +import * as THREE from 'three'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import gsap from 'gsap'; +import { toast } from 'react-toastify'; +import { useSocketStore, useActiveLayer, useWallItems, useFloorItems, useLayers, useUpdateScene, useWalls, useDeletedLines, useNewLines, useZonePoints, useZones } from "../../store/store"; + +import * as Types from "../../types/world/worldTypes"; +import * as CONSTANTS from '../../types/world/worldConstants'; +import TempLoader from "../builder/geomentries/assets/tempLoader"; + +// import { setFloorItemApi } from "../../services/factoryBuilder/assest/floorAsset/setFloorItemApi"; +import objectLineToArray from "../builder/geomentries/lines/lineConvertions/objectLineToArray"; +import addLineToScene from "../builder/geomentries/lines/addLineToScene"; +import updateLinesPositions from "../builder/geomentries/lines/updateLinesPositions"; +import updateLines from "../builder/geomentries/lines/updateLines"; +import updateDistanceText from "../builder/geomentries/lines/updateDistanceText"; +import updateFloorLines from "../builder/geomentries/floors/updateFloorLines"; +import loadWalls from "../builder/geomentries/walls/loadWalls"; +import RemoveConnectedLines from "../builder/geomentries/lines/removeConnectedLines"; +import Layer2DVisibility from "../builder/geomentries/layers/layer2DVisibility"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; +import { retrieveGLTF, storeGLTF } from "../../utils/indexDB/idbUtils"; +import { getZonesApi } from "../../services/factoryBuilder/zones/getZonesApi"; + + +export default function SocketResponses({ + floorPlanGroup, + lines, + floorGroup, + floorGroupAisle, + scene, + onlyFloorlines, + AssetConfigurations, + itemsGroup, + isTempLoader, + tempLoader, + currentLayerPoint, + floorPlanGroupPoint, + floorPlanGroupLine, + zoneGroup, + dragPointControls +}: any) { + + const { socket } = useSocketStore(); + const { activeLayer, setActiveLayer } = useActiveLayer(); + const { wallItems, setWallItems } = useWallItems(); + const { layers, setLayers } = useLayers(); + const { floorItems, setFloorItems } = useFloorItems(); + const { updateScene, setUpdateScene } = useUpdateScene(); + const { walls, setWalls } = useWalls(); + const { deletedLines, setDeletedLines } = useDeletedLines(); + const { newLines, setNewLines } = useNewLines(); + const { zones, setZones } = useZones(); + const { zonePoints, setZonePoints } = useZonePoints(); + + useEffect(() => { + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + if (!socket) return + + socket.on('cameraCreateResponse', (data: any) => { + // console.log('data: ', data); + }) + + socket.on('userConnectRespones', (data: any) => { + // console.log('data: ', data); + }) + + socket.on('userDisConnectRespones', (data: any) => { + // console.log('data: ', data); + }) + + socket.on('cameraUpdateResponse', (data: any) => { + // console.log('data: ', data); + }) + + socket.on('EnvironmentUpdateResponse', (data: any) => { + // console.log('data: ', data); + }) + + socket.on('FloorItemsUpdateResponse', async (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "flooritem created") { + 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); + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + + try { + isTempLoader.current = true; + const cachedModel = THREE.Cache.get(data.data.modelname); + let url; + + if (cachedModel) { + // console.log(`Getting ${data.data.modelname} from cache`); + url = URL.createObjectURL(cachedModel); + } else { + const indexedDBModel = await retrieveGLTF(data.data.modelname); + if (indexedDBModel) { + // console.log(`Getting ${data.data.modelname} from IndexedDB`); + url = URL.createObjectURL(indexedDBModel); + } else { + // console.log(`Getting ${data.data.modelname} from Backend`); + url = `${url_Backend_dwinzo}/api/v1/AssetFile/${data.data.modelfileID}`; + const modelBlob = await fetch(url).then((res) => res.blob()); + await storeGLTF(data.data.modelfileID, modelBlob); + } + } + + loadModel(url); + + } catch (error) { + console.error('Error fetching asset model:', error); + } + + function loadModel(url: string) { + loader.load(url, (gltf) => { + URL.revokeObjectURL(url); + THREE.Cache.remove(url); + const model = gltf.scene; + model.uuid = data.data.modeluuid; + model.userData = { name: data.data.modelname, modelId: data.data.modelFileID }; + model.position.set(...data.data.position as [number, number, number]); + model.rotation.set(data.data.rotation.x, data.data.rotation.y, data.data.rotation.z); + model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); + + model.traverse((child: any) => { + if (child.isMesh) { + // Clone the material to ensure changes are independent + // child.material = child.material.clone(); + + child.castShadow = true; + child.receiveShadow = true; + } + }); + + itemsGroup.current.add(model); + + if (tempLoader.current) { + tempLoader.current.material.dispose(); + tempLoader.current.geometry.dispose(); + itemsGroup.current.remove(tempLoader.current); + tempLoader.current = undefined; + } + + const newFloorItem: Types.FloorItemType = { + modeluuid: data.data.modeluuid, + modelname: data.data.modelname, + modelfileID: data.data.modelfileID, + position: [...data.data.position as [number, number, number]], + rotation: { + x: model.rotation.x, + y: model.rotation.y, + z: model.rotation.z, + }, + isLocked: data.data.isLocked, + isVisible: data.data.isVisible, + }; + + setFloorItems((prevItems: any) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + gsap.to(model.position, { y: data.data.position[1], duration: 1.5, ease: 'power2.out' }); + gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: 'power2.out', onComplete: () => { toast.success("Model Added!") } }); + + THREE.Cache.add(data.data.modelname, gltf); + }, () => { + TempLoader(new THREE.Vector3(...data.data.position), isTempLoader, tempLoader, itemsGroup); + }); + } + + } else if (data.message === "flooritems updated") { + itemsGroup.current.children.forEach((item: THREE.Group) => { + if (item.uuid === data.data.modeluuid) { + item.position.set(...data.data.position as [number, number, number]); + item.rotation.set(data.data.rotation.x, data.data.rotation.y, data.data.rotation.z); + } + }) + + setFloorItems((prevItems: Types.FloorItems) => { + if (!prevItems) { + return + } + let updatedItem: any = null; + const updatedItems = prevItems.map((item) => { + if (item.modeluuid === data.data.modeluuid) { + updatedItem = { + ...item, + position: [...data.data.position] as [number, number, number], + rotation: { x: data.data.rotation.x, y: data.data.rotation.y, z: data.data.rotation.z, }, + }; + return updatedItem; + } + return item; + }); + return updatedItems; + }) + } + }) + + socket.on('FloorItemsDeleteResponse', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "flooritem deleted") { + const deletedUUID = data.data.modeluuid; + let items = JSON.parse(localStorage.getItem("FloorItems")!); + + const updatedItems = items.filter( + (item: { modeluuid: string }) => item.modeluuid !== deletedUUID + ); + + const storedItems = JSON.parse(localStorage.getItem("FloorItems") || '[]'); + const updatedStoredItems = storedItems.filter((item: { modeluuid: string }) => item.modeluuid !== deletedUUID); + localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); + + itemsGroup.current.children.forEach((item: any) => { + if (item.uuid === deletedUUID) { + itemsGroup.current.remove(item); + } + }) + setFloorItems(updatedItems); + toast.success("Model Removed!"); + } + }) + + socket.on('Line:response:update', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "line updated") { + const DraggedUUID = data.data.uuid; + const DraggedPosition = new THREE.Vector3(data.data.position.x, data.data.position.y, data.data.position.z); + + const point = floorPlanGroupPoint.current.getObjectByProperty('uuid', DraggedUUID); + point.position.set(DraggedPosition.x, DraggedPosition.y, DraggedPosition.z); + const affectedLines = updateLinesPositions({ uuid: DraggedUUID, position: DraggedPosition }, lines); + + updateLines(floorPlanGroupLine, affectedLines); + updateDistanceText(scene, floorPlanGroupLine, affectedLines); + updateFloorLines(onlyFloorlines, { uuid: DraggedUUID, position: DraggedPosition }); + + loadWalls(lines, setWalls); + setUpdateScene(true); + + } + }) + + socket.on('Line:response:delete', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "line deleted") { + const line = objectLineToArray(data.data); + const linePoints = line; + const connectedpoints = [linePoints[0][1], linePoints[1][1]]; + + + onlyFloorlines.current = onlyFloorlines.current.map((floorline: any) => + floorline.filter((line: any) => line[0][1] !== connectedpoints[0] && line[1][1] !== connectedpoints[1]) + ).filter((floorline: any) => floorline.length > 0); + + const removedLine = lines.current.find((item: any) => (item[0][1] === linePoints[0][1] && item[1][1] === linePoints[1][1] || (item[0][1] === linePoints[1][1] && item[1][1] === linePoints[0][1]))); + lines.current = lines.current.filter((item: any) => item !== removedLine); + + floorPlanGroupLine.current.children.forEach((line: any) => { + const linePoints = line.userData.linePoints as [number, string, number][]; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + + if ((uuid1 === connectedpoints[0] && uuid2 === connectedpoints[1] || (uuid1 === connectedpoints[1] && uuid2 === connectedpoints[0]))) { + line.material.dispose(); + line.geometry.dispose(); + floorPlanGroupLine.current.remove(line); + setDeletedLines([line.userData.linePoints]) + } + }); + + connectedpoints.forEach((pointUUID) => { + let isConnected = false; + floorPlanGroupLine.current.children.forEach((line: any) => { + const linePoints = line.userData.linePoints; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + if (uuid1 === pointUUID || uuid2 === pointUUID) { + isConnected = true; + } + }); + + if (!isConnected) { + floorPlanGroupPoint.current.children.forEach((point: any) => { + if (point.uuid === pointUUID) { + point.material.dispose(); + point.geometry.dispose(); + floorPlanGroupPoint.current.remove(point); + } + }); + } + }); + + loadWalls(lines, setWalls); + setUpdateScene(true); + + toast.success("Line Removed!"); + } + }) + + socket.on('Line:response:delete:point', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "point deleted") { + const point = floorPlanGroupPoint.current?.getObjectByProperty('uuid', data.data); + point.material.dispose(); + point.geometry.dispose(); + floorPlanGroupPoint.current.remove(point); + + onlyFloorlines.current = onlyFloorlines.current.map((floorline: any) => + floorline.filter((line: any) => line[0][1] !== data.data && line[1][1] !== data.data) + ).filter((floorline: any) => floorline.length > 0); + + RemoveConnectedLines(data.data, floorPlanGroupLine, floorPlanGroupPoint, setDeletedLines, lines); + + loadWalls(lines, setWalls); + setUpdateScene(true); + + toast.success("Point Removed!"); + } + }) + + socket.on('Line:response:delete:layer', async (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "layer deleted") { + setActiveLayer(1) + const removedLayer = data.data; + const removedLines: Types.Lines = lines.current.filter((line: any) => line[0][2] === removedLayer); + + ////////// Remove Points and lines from the removed layer ////////// + + removedLines.forEach(async (line) => { + line.forEach(async (removedPoint) => { + const removableLines: THREE.Mesh[] = []; + const connectedpoints: string[] = []; + + floorPlanGroupLine.current.children.forEach((line: any) => { + const linePoints = line.userData.linePoints as [number, string, number][]; + const uuid1 = linePoints[0][1]; + const uuid2 = linePoints[1][1]; + + if (uuid1 === removedPoint[1] || uuid2 === removedPoint[1]) { + connectedpoints.push(uuid1 === removedPoint[1] ? uuid2 : uuid1); + removableLines.push(line as THREE.Mesh); + } + }); + + if (removableLines.length > 0) { + removableLines.forEach((line: any) => { + lines.current = lines.current.filter((item: any) => JSON.stringify(item) !== JSON.stringify(line.userData.linePoints)); + line.material.dispose(); + line.geometry.dispose(); + floorPlanGroupLine.current.remove(line); + }); + } + + const point = floorPlanGroupPoint.current.getObjectByProperty('uuid', removedPoint[1]); + if (point) { + point.material.dispose(); + point.geometry.dispose(); + floorPlanGroupPoint.current.remove(point) + } + }); + }); + + ////////// Update the remaining lines layer values in the userData and in lines.current ////////// + + let remaining = lines.current.filter((line: any) => line[0][2] !== removedLayer); + let updatedLines: Types.Lines = []; + remaining.forEach((line: any) => { + let newLines = JSON.parse(JSON.stringify(line)); + if (newLines[0][2] > removedLayer) { + newLines[0][2] -= 1; + newLines[1][2] -= 1; + } + + const matchingLine = floorPlanGroupLine.current.children.find((l: any) => l.userData.linePoints[0][1] === line[0][1] && l.userData.linePoints[1][1] === line[1][1]); + if (matchingLine) { + const updatedUserData = JSON.parse(JSON.stringify(matchingLine.userData)); + updatedUserData.linePoints[0][2] = newLines[0][2]; + updatedUserData.linePoints[1][2] = newLines[1][2]; + matchingLine.userData = updatedUserData; + } + updatedLines.push(newLines); + }); + + lines.current = updatedLines; + localStorage.setItem("Lines", JSON.stringify(lines.current)); + + ////////// Also remove OnlyFloorLines and update it in localstorage ////////// + + onlyFloorlines.current = onlyFloorlines.current.filter((floor: any) => { + return floor[0][0][2] !== removedLayer; + }); + const meshToRemove = floorGroup.current?.children.find((mesh: any) => + mesh.name === `Only_Floor_Line_${removedLayer}` + ); + if (meshToRemove) { + meshToRemove.geometry.dispose(); + meshToRemove.material.dispose(); + floorGroup.current?.remove(meshToRemove); + } + + const zonesData = await getZonesApi(organization); + const highestLayer = Math.max( + 1, + lines.current.reduce((maxLayer: number, segment: any) => Math.max(maxLayer, segment.layer || 0), 0), + zonesData.data.reduce((maxLayer: number, zone: any) => Math.max(maxLayer, zone.layer || 0), 0) + ); + + setLayers(highestLayer); + + loadWalls(lines, setWalls); + setUpdateScene(true); + + toast.success("Layer Removed!"); + } + }) + }, [socket]) + + useEffect(() => { + if (!socket) return + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + socket.on('wallItemsDeleteResponse', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "wallitem deleted") { + const deletedUUID = data.data.modeluuid; + let WallItemsRef = wallItems; + const Items = WallItemsRef.filter((item: any) => item.model?.uuid !== deletedUUID); + + setWallItems([]); + setTimeout(async () => { + WallItemsRef = Items; + setWallItems(WallItemsRef); + const WallItemsForStorage = WallItemsRef.map((item: any) => { + const { model, ...rest } = item; + return { + ...rest, + modeluuid: model?.uuid, + }; + }); + + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + toast.success("Model Removed!"); + }, 50); + } + }) + + socket.on('wallItemsUpdateResponse', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "wallIitem created") { + const loader = new GLTFLoader(); + loader.load(AssetConfigurations[data.data.modelname].modelUrl, async (gltf) => { + const model = gltf.scene; + model.uuid = data.data.modeluuid; + model.children[0].children.forEach((child) => { + if (child.name !== "CSG_REF") { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + const newWallItem = { + type: data.data.type, + model: model, + modelname: data.data.modelname, + scale: data.data.scale, + csgscale: data.data.csgscale, + csgposition: data.data.csgposition, + position: data.data.position, + quaternion: data.data.quaternion + }; + + setWallItems((prevItems: any) => { + const updatedItems = [...prevItems, newWallItem]; + + const WallItemsForStorage = updatedItems.map(item => { + const { model, ...rest } = item; + return { + ...rest, + modeluuid: model?.uuid, + }; + }); + + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + toast.success("Model Added!"); + + return updatedItems; + }); + }); + } else if (data.message === "wallIitem updated") { + const updatedUUID = data.data.modeluuid; + + setWallItems((prevItems: any) => { + const updatedItems = prevItems.map((item: any) => { + if (item.model.uuid === updatedUUID) { + return { + ...item, + position: data.data.position, + quaternion: data.data.quaternion, + scale: data.data.scale, + csgscale: data.data.csgscale, + csgposition: data.data.csgposition, + }; + } + return item; + }); + + const WallItemsForStorage = updatedItems.map((item: any) => { + const { model, ...rest } = item; + return { + ...rest, + modeluuid: model?.uuid, + }; + }); + + localStorage.setItem("WallItems", JSON.stringify(WallItemsForStorage)); + toast.success("Model Updated!"); + + return updatedItems; + }); + + } + + }) + + return () => { + socket.off('wallItemsDeleteResponse'); + socket.off('wallItemsUpdateResponse'); + }; + }, [wallItems]) + + function getPointColor(lineType: string | undefined): string { + switch (lineType) { + case CONSTANTS.lineConfig.wallName: return CONSTANTS.pointConfig.wallOuterColor; + case CONSTANTS.lineConfig.floorName: return CONSTANTS.pointConfig.floorOuterColor; + case CONSTANTS.lineConfig.aisleName: return CONSTANTS.pointConfig.aisleOuterColor; + default: return CONSTANTS.pointConfig.defaultOuterColor; + } + } + + function getLineColor(lineType: string | undefined): string { + switch (lineType) { + case CONSTANTS.lineConfig.wallName: return CONSTANTS.lineConfig.wallColor; + case CONSTANTS.lineConfig.floorName: return CONSTANTS.lineConfig.floorColor; + case CONSTANTS.lineConfig.aisleName: return CONSTANTS.lineConfig.aisleColor; + default: return CONSTANTS.lineConfig.defaultColor; + } + } + + useEffect(() => { + if (!socket) return + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + socket.on('Line:response:create', async (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "line create") { + const line: Types.Line = objectLineToArray(data.data); + const type = line[0][3]; + const pointColour = getPointColor(type); + const lineColour = getLineColor(type); + setNewLines([line]) + + line.forEach((line) => { + const existingPoint = floorPlanGroupPoint.current?.getObjectByProperty('uuid', line[1]); + if (existingPoint) { + return; + } + const geometry = new THREE.BoxGeometry(...CONSTANTS.pointConfig.boxScale); + const material = new THREE.ShaderMaterial({ + uniforms: { + uColor: { value: new THREE.Color(pointColour) }, // Blue color for the border + uInnerColor: { value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor) }, // White color for the inner square + }, + vertexShader: ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform vec3 uColor; + uniform vec3 uInnerColor; + + void main() { + // Define the size of the white square as a proportion of the face + float borderThickness = 0.2; // Adjust this value for border thickness + if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness && + vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) { + gl_FragColor = vec4(uInnerColor, 1.0); // White inner square + } else { + gl_FragColor = vec4(uColor, 1.0); // Blue border + } + } + `, + }); + const point = new THREE.Mesh(geometry, material); + point.name = "point"; + point.uuid = line[1]; + point.userData = { type: type, color: pointColour }; + point.position.set(line[0].x, line[0].y, line[0].z); + currentLayerPoint.current.push(point); + + floorPlanGroupPoint.current?.add(point); + }) + if (dragPointControls.current) { + dragPointControls.current!.objects = currentLayerPoint.current; + } + addLineToScene( + new THREE.Vector3(line[0][0].x, line[0][0].y, line[0][0].z), + new THREE.Vector3(line[1][0].x, line[1][0].y, line[1][0].z), + lineColour, + line, + floorPlanGroupLine + ) + lines.current.push(line); + + const zonesData = await getZonesApi(organization); + const highestLayer = Math.max( + 1, + lines.current.reduce((maxLayer: number, segment: any) => Math.max(maxLayer, segment.layer || 0), 0), + zonesData.data.reduce((maxLayer: number, zone: any) => Math.max(maxLayer, zone.layer || 0), 0) + ); + + setLayers(highestLayer); + + Layer2DVisibility(activeLayer, floorPlanGroup, floorPlanGroupLine, floorPlanGroupPoint, currentLayerPoint, dragPointControls) + + loadWalls(lines, setWalls); + setUpdateScene(true); + } + }) + + return () => { + socket.off('Line:response:create'); + }; + }, [socket, activeLayer]) + + useEffect(() => { + if (!socket) return + const email = localStorage.getItem('email'); + const organization = (email!.split("@")[1]).split(".")[0]; + + socket.on('zone:response:updates', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + + if (data.message === "zone created") { + const pointsArray: [number, number, number][] = data.data.points; + const vector3Array = pointsArray.map(([x, y, z]) => new THREE.Vector3(x, y, z)); + const newZones = [...zones, data.data]; + setZones(newZones); + const updatedZonePoints = [...zonePoints, ...vector3Array]; + setZonePoints(updatedZonePoints); + + const highestLayer = Math.max( + 1, + lines.current.reduce((maxLayer: number, segment: any) => Math.max(maxLayer, segment.layer || 0), 0), + newZones.reduce((maxLayer: number, zone: any) => Math.max(maxLayer, zone.layer || 0), 0) + ); + + setLayers(highestLayer); + setUpdateScene(true); + } + + if (data.message === "zone updated") { + const updatedZones = zones.map((zone: any) => + zone.zoneId === data.data.zoneId ? data.data : zone + ); + setZones(updatedZones); + setUpdateScene(true); + } + + + }) + + socket.on('zone:response:delete', (data: any) => { + if (socket.id === data.socketId) { + return + } + if (organization !== data.organization) { + return + } + if (data.message === "zone deleted") { + const updatedZones = zones.filter((zone: any) => zone.zoneId !== data.data.zoneId); + setZones(updatedZones); + + const zoneIndex = zones.findIndex((zone: any) => zone.zoneId === data.data.zoneId); + if (zoneIndex !== -1) { + const updatedzonePoints = zonePoints.filter((_: any, index: any) => index < zoneIndex * 4 || index >= zoneIndex * 4 + 4); + setZonePoints(updatedzonePoints); + } + + const highestLayer = Math.max( + 1, + lines.current.reduce((maxLayer: number, segment: any) => Math.max(maxLayer, segment.layer || 0), 0), + updatedZones.reduce((maxLayer: number, zone: any) => Math.max(maxLayer, zone.layer || 0), 0) + ); + + setLayers(highestLayer); + setUpdateScene(true); + } + }) + + return () => { + socket.off('zone:response:updates'); + socket.off('zone:response:updates'); + }; + }, [socket, zones, zonePoints]) + + return ( + <> + ) +} \ No newline at end of file diff --git a/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts b/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts new file mode 100644 index 0000000..6633fe4 --- /dev/null +++ b/app/src/modules/scene/IntialLoad/loadInitialFloorItems.ts @@ -0,0 +1,202 @@ +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import gsap from 'gsap'; +import * as THREE from 'three'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import { toast } from 'react-toastify'; +import * as Types from "../../../types/world/worldTypes"; +import { getFloorItems } from '../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi'; +import { initializeDB, retrieveGLTF, storeGLTF } from '../../../utils/indexDB/idbUtils'; +import { getCamera } from '../../../services/factoryBuilder/camera/getCameraApi'; + +async function loadInitialFloorItems( + itemsGroup: Types.RefGroup, + setFloorItems: Types.setFloorItemSetState +): Promise { + if (!itemsGroup.current) return; + let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + const email = localStorage.getItem('email'); + const organization = (email!.split("@")[1]).split(".")[0]; + + const items = await getFloorItems(organization); + localStorage.setItem("FloorItems", JSON.stringify(items)); + await initializeDB(); + + if (items) { + const storedFloorItems: Types.FloorItems = items; + 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); + + let modelsLoaded = 0; + const modelsToLoad = storedFloorItems.length; + + const camData = await getCamera(organization, localStorage.getItem('userId')!); + let storedPosition; + if (camData && camData.position) { + storedPosition = camData?.position; + } else { + storedPosition = new THREE.Vector3(0, 40, 30); + } + if (!storedPosition) return; + const cameraPosition = new THREE.Vector3(storedPosition.x, storedPosition.y, storedPosition.z); + + storedFloorItems.sort((a, b) => { + const aPosition = new THREE.Vector3(a.position[0], a.position[1], a.position[2]); + const bPosition = new THREE.Vector3(b.position[0], b.position[1], b.position[2]); + return cameraPosition.distanceTo(aPosition) - cameraPosition.distanceTo(bPosition); + }); + + for (const item of storedFloorItems) { + if (!item.modelfileID) return; + const itemPosition = new THREE.Vector3(item.position[0], item.position[1], item.position[2]); + let storedPosition; + if (localStorage.getItem("cameraPosition")) { + storedPosition = JSON.parse(localStorage.getItem("cameraPosition")!); + } else { + storedPosition = new THREE.Vector3(0, 40, 30); + } + + const cameraPosition = new THREE.Vector3(storedPosition.x, storedPosition.y, storedPosition.z); + + if (cameraPosition.distanceTo(itemPosition) < 50) { + await new Promise(async (resolve) => { + + // Check Three.js Cache + const cachedModel = THREE.Cache.get(item.modelfileID!); + if (cachedModel) { + // console.log(`[Cache] Fetching ${item.modelname}`); + processLoadedModel(cachedModel.scene.clone(), item, itemsGroup, setFloorItems); + modelsLoaded++; + checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve); + return; + } + + // Check IndexedDB + const indexedDBModel = await retrieveGLTF(item.modelfileID!); + if (indexedDBModel) { + // console.log(`[IndexedDB] Fetching ${item.modelname}`); + const blobUrl = URL.createObjectURL(indexedDBModel); + loader.load( + blobUrl, + (gltf) => { + URL.revokeObjectURL(blobUrl); + THREE.Cache.remove(blobUrl); + THREE.Cache.add(item.modelfileID!, gltf); + processLoadedModel(gltf.scene.clone(), item, itemsGroup, setFloorItems); + modelsLoaded++; + checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve); + }, + undefined, + (error) => { + toast.error(`[IndexedDB] Error loading ${item.modelname}:`); + URL.revokeObjectURL(blobUrl); + resolve(); + } + ); + return; + } + + // Fetch from Backend + // console.log(`[Backend] Fetching ${item.modelname}`); + const modelUrl = `${url_Backend_dwinzo}/api/v1/AssetFile/${item.modelfileID!}`; + loader.load( + modelUrl, + async (gltf) => { + const modelBlob = await fetch(modelUrl).then((res) => res.blob()); + await storeGLTF(item.modelfileID!, modelBlob); + THREE.Cache.add(item.modelfileID!, gltf); + processLoadedModel(gltf.scene.clone(), item, itemsGroup, setFloorItems); + modelsLoaded++; + checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, resolve); + }, + undefined, + (error) => { + toast.error(`[Backend] Error loading ${item.modelname}:`); + resolve(); + } + ); + }); + } else { + // console.log(`Item ${item.modelname} is not near`); + setFloorItems((prevItems) => [ + ...(prevItems || []), + { + modeluuid: item.modeluuid, + modelname: item.modelname, + position: item.position, + rotation: item.rotation, + modelfileID: item.modelfileID, + isLocked: item.isLocked, + isVisible: item.isVisible, + }, + ]); + modelsLoaded++; + checkLoadingCompletion(modelsLoaded, modelsToLoad, dracoLoader, () => { }); + } + } + + // Dispose loader after all models + dracoLoader.dispose(); + } +} + + +function processLoadedModel( + gltf: any, + item: Types.FloorItemType, + itemsGroup: Types.RefGroup, + setFloorItems: Types.setFloorItemSetState +) { + const model = gltf; + model.uuid = item.modeluuid; + model.scale.set(...CONSTANTS.assetConfig.defaultScaleBeforeGsap); + model.userData = { name: item.modelname, modelId: item.modelfileID }; + model.position.set(...item.position); + model.rotation.set(item.rotation.x, item.rotation.y, item.rotation.z); + + model.traverse((child: any) => { + if (child.isMesh) { + // Clone the material to ensure changes are independent + // child.material = child.material.clone(); + + child.castShadow = true; + child.receiveShadow = true; + } + }); + + + itemsGroup?.current?.add(model); + setFloorItems((prevItems) => [ + ...(prevItems || []), + { + modeluuid: item.modeluuid, + modelname: item.modelname, + position: item.position, + rotation: item.rotation, + modelfileID: item.modelfileID, + isLocked: item.isLocked, + isVisible: item.isVisible, + }, + ]); + + gsap.to(model.position, { y: item.position[1], duration: 1.5, ease: 'power2.out' }); + gsap.to(model.scale, { x: 1, y: 1, z: 1, duration: 1.5, ease: 'power2.out' }); +} + +function checkLoadingCompletion( + modelsLoaded: number, + modelsToLoad: number, + dracoLoader: DRACOLoader, + resolve: () => void +) { + if (modelsLoaded === modelsToLoad) { + toast.success("Models Loaded!"); + dracoLoader.dispose(); + } + resolve(); +} + +export default loadInitialFloorItems; \ No newline at end of file diff --git a/app/src/modules/scene/IntialLoad/loadInitialLine.ts b/app/src/modules/scene/IntialLoad/loadInitialLine.ts new file mode 100644 index 0000000..f8c3132 --- /dev/null +++ b/app/src/modules/scene/IntialLoad/loadInitialLine.ts @@ -0,0 +1,30 @@ +import addLineToScene from '../../builder/geomentries/lines/addLineToScene'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import * as Types from "../../../types/world/worldTypes"; + +function loadInitialLine( + floorPlanGroupLine: Types.RefGroup, + lines: Types.RefLines +): void { + + if (!floorPlanGroupLine.current) return + + ////////// Load the Lines initially if there are any ////////// + + floorPlanGroupLine.current.children = []; + lines.current.forEach((line) => { + let colour; + if (line[0][3] && line[1][3] === CONSTANTS.lineConfig.wallName) { + colour = CONSTANTS.lineConfig.wallColor; + } else if (line[0][3] && line[1][3] === CONSTANTS.lineConfig.floorName) { + colour = CONSTANTS.lineConfig.floorColor; + } else if (line[0][3] && line[1][3] === CONSTANTS.lineConfig.aisleName) { + colour = CONSTANTS.lineConfig.aisleColor; + } + if (colour) { + addLineToScene(line[0][0], line[1][0], colour, line, floorPlanGroupLine); + } + }); +} + +export default loadInitialLine; diff --git a/app/src/modules/scene/IntialLoad/loadInitialPoint.ts b/app/src/modules/scene/IntialLoad/loadInitialPoint.ts new file mode 100644 index 0000000..7dfdf1d --- /dev/null +++ b/app/src/modules/scene/IntialLoad/loadInitialPoint.ts @@ -0,0 +1,87 @@ +import * as THREE from 'three'; + +import * as CONSTANTS from '../../../types/world/worldConstants'; +import * as Types from "../../../types/world/worldTypes"; + +////////// Load the Boxes initially if there are any ////////// + +function loadInitialPoint( + lines: Types.RefLines, + floorPlanGroupPoint: Types.RefGroup, + currentLayerPoint: Types.RefMeshArray, + dragPointControls: Types.RefDragControl +): void { + + if (!floorPlanGroupPoint.current) return + + floorPlanGroupPoint.current.children = []; + currentLayerPoint.current = []; + lines.current.forEach((line) => { + const colour = getPointColor(line[0][3]); + line.forEach((pointData) => { + const [point, id] = pointData; + + /////////// Check if a box with this id already exists ////////// + + const existingBox = floorPlanGroupPoint.current?.getObjectByProperty('uuid', id); + if (existingBox) { + return; + } + + const geometry = new THREE.BoxGeometry(...CONSTANTS.pointConfig.boxScale); + const material = new THREE.ShaderMaterial({ + uniforms: { + uColor: { value: new THREE.Color(colour) }, // Blue color for the border + uInnerColor: { value: new THREE.Color(CONSTANTS.pointConfig.defaultInnerColor) }, // White color for the inner square + }, + vertexShader: ` + varying vec2 vUv; + + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } + `, + fragmentShader: ` + varying vec2 vUv; + uniform vec3 uColor; + uniform vec3 uInnerColor; + + void main() { + // Define the size of the white square as a proportion of the face + float borderThickness = 0.2; // Adjust this value for border thickness + if (vUv.x > borderThickness && vUv.x < 1.0 - borderThickness && + vUv.y > borderThickness && vUv.y < 1.0 - borderThickness) { + gl_FragColor = vec4(uInnerColor, 1.0); // White inner square + } else { + gl_FragColor = vec4(uColor, 1.0); // Blue border + } + } + `, + }); + const box = new THREE.Mesh(geometry, material); + box.name = "point"; + box.uuid = id; + box.userData = { type: line[0][3], color: colour }; + box.position.set(point.x, point.y, point.z); + currentLayerPoint.current.push(box); + + floorPlanGroupPoint.current?.add(box); + }); + }); + + function getPointColor(lineType: string | undefined): string { + switch (lineType) { + case CONSTANTS.lineConfig.wallName: return CONSTANTS.pointConfig.wallOuterColor; + case CONSTANTS.lineConfig.floorName: return CONSTANTS.pointConfig.floorOuterColor; + case CONSTANTS.lineConfig.aisleName: return CONSTANTS.pointConfig.aisleOuterColor; + default: return CONSTANTS.pointConfig.defaultOuterColor; + } + } + + if (dragPointControls.current) { + dragPointControls.current!.objects = currentLayerPoint.current; + } +} + +export default loadInitialPoint; diff --git a/app/src/modules/scene/IntialLoad/loadInitialWallItems.ts b/app/src/modules/scene/IntialLoad/loadInitialWallItems.ts new file mode 100644 index 0000000..34273af --- /dev/null +++ b/app/src/modules/scene/IntialLoad/loadInitialWallItems.ts @@ -0,0 +1,54 @@ +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; + +import * as Types from "../../../types/world/worldTypes"; +import { getWallItems } from '../../../services/factoryBuilder/assest/wallAsset/getWallItemsApi'; + +////////// Load the Wall Items's intially of there is any ////////// + +async function loadInitialWallItems( + setWallItems: Types.setWallItemSetState, + AssetConfigurations: Types.AssetConfigurations +): Promise { + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const items = await getWallItems(organization); + + localStorage.setItem("WallItems", JSON.stringify(items)); + if (items.length > 0) { + const storedWallItems: Types.wallItems = items; + + const loadedWallItems = await Promise.all(storedWallItems.map(async (item) => { + const loader = new GLTFLoader(); + return new Promise((resolve) => { + loader.load(AssetConfigurations[item.modelname!].modelUrl, (gltf) => { + const model = gltf.scene; + model.uuid = item.modeluuid!; + + model.children[0].children.forEach((child: any) => { + if (child.name !== "CSG_REF") { + child.castShadow = true; + child.receiveShadow = true; + } + }); + + resolve({ + type: item.type, + model: model, + modelname: item.modelname, + scale: item.scale, + csgscale: item.csgscale, + csgposition: item.csgposition, + position: item.position, + quaternion: item.quaternion, + }); + }); + }); + })); + + setWallItems(loadedWallItems); + } +} + +export default loadInitialWallItems; diff --git a/app/src/modules/scene/camera/camMode.tsx b/app/src/modules/scene/camera/camMode.tsx new file mode 100644 index 0000000..dbab5e7 --- /dev/null +++ b/app/src/modules/scene/camera/camMode.tsx @@ -0,0 +1,89 @@ +import { useFrame, useThree } from '@react-three/fiber'; +import React, { useEffect, useState } from 'react'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +import { useCamMode, useToggleView } from '../../../store/store'; +import { useKeyboardControls } from '@react-three/drei'; +import switchToThirdPerson from './switchToThirdPerson'; +import switchToFirstPerson from './switchToFirstPerson'; + +const CamMode: React.FC = () => { + const { camMode, setCamMode } = useCamMode(); + const [, get] = useKeyboardControls() + const [isTransitioning, setIsTransitioning] = useState(false); + const state: any = useThree(); + const { toggleView } = useToggleView(); + + useEffect(() => { + const handlePointerLockChange = async () => { + if (document.pointerLockElement && !toggleView) { + // console.log('Pointer is locked'); + } else { + // console.log('Pointer is unlocked'); + if (camMode === "FirstPerson" && !toggleView) { + setCamMode("ThirdPerson"); + await switchToThirdPerson(state.controls, state.camera); + } + } + }; + + document.addEventListener('pointerlockchange', handlePointerLockChange); + + return () => { + document.removeEventListener('pointerlockchange', handlePointerLockChange); + }; + }, [camMode, toggleView, setCamMode, state.controls, state.camera]); + + useEffect(() => { + const handleKeyPress = async (event: any) => { + if (!state.controls) return; + + if (event.key === "/" && !isTransitioning && !toggleView) { + setIsTransitioning(true); + state.controls.mouseButtons.left = CONSTANTS.controlsTransition.leftMouse; + state.controls.mouseButtons.right = CONSTANTS.controlsTransition.rightMouse; + state.controls.mouseButtons.wheel = CONSTANTS.controlsTransition.wheelMouse; + state.controls.mouseButtons.middle = CONSTANTS.controlsTransition.middleMouse; + + if (camMode === 'ThirdPerson') { + setCamMode("FirstPerson"); + await switchToFirstPerson(state.controls, state.camera); + } else if (camMode === "FirstPerson") { + setCamMode("ThirdPerson"); + await switchToThirdPerson(state.controls, state.camera); + } + + setIsTransitioning(false); + } + }; + + window.addEventListener("keydown", handleKeyPress); + return () => { + window.removeEventListener("keydown", handleKeyPress); + }; + }, [camMode, isTransitioning, toggleView, state.controls, state.camera, setCamMode]); + + useFrame(() => { + const { forward, backward, left, right } = get(); + if (!state.controls) return + if (!state.controls || camMode === "ThirdPerson" || !document.pointerLockElement) return; + + if (forward) { + state.controls.forward(CONSTANTS.firstPersonControls.forwardSpeed, true) + } + if (backward) { + state.controls.forward(CONSTANTS.firstPersonControls.backwardSpeed, true) + } + if (left) { + state.controls.truck(CONSTANTS.firstPersonControls.leftSpeed, 0, true) + } + if (right) { + state.controls.truck(CONSTANTS.firstPersonControls.rightSpeed, 0, true) + } + }); + + return ( + <> + ); +}; + +export default CamMode; \ No newline at end of file diff --git a/app/src/modules/scene/camera/switchToFirstPerson.ts b/app/src/modules/scene/camera/switchToFirstPerson.ts new file mode 100644 index 0000000..6746220 --- /dev/null +++ b/app/src/modules/scene/camera/switchToFirstPerson.ts @@ -0,0 +1,25 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../types/world/worldConstants'; + +export default async function switchToFirstPerson( + controls: any, + camera: any +) { + if (!controls) return; + + const cameraDirection = new THREE.Vector3(); + camera.getWorldDirection(cameraDirection); + cameraDirection.normalize(); + + await controls.setPosition(camera.position.x, 2, camera.position.z, true); + controls.setTarget(camera.position.x, 2, camera.position.z, true); + controls.mouseButtons.left = CONSTANTS.firstPersonControls.leftMouse; + controls.lockPointer(); + + controls.azimuthRotateSpeed = CONSTANTS.firstPersonControls.azimuthRotateSpeed; + controls.polarRotateSpeed = CONSTANTS.firstPersonControls.polarRotateSpeed; + controls.truckSpeed = CONSTANTS.firstPersonControls.truckSpeed; + controls.minDistance = CONSTANTS.firstPersonControls.minDistance; + controls.maxDistance = CONSTANTS.firstPersonControls.maxDistance; + controls.maxPolarAngle = CONSTANTS.firstPersonControls.maxPolarAngle; +} \ No newline at end of file diff --git a/app/src/modules/scene/camera/switchToThirdPerson.ts b/app/src/modules/scene/camera/switchToThirdPerson.ts new file mode 100644 index 0000000..f43bc79 --- /dev/null +++ b/app/src/modules/scene/camera/switchToThirdPerson.ts @@ -0,0 +1,29 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../../types/world/worldConstants'; + +export default async function switchToThirdPerson( + controls: any, + camera: any +) { + if (!controls) return; + controls.mouseButtons.left = CONSTANTS.thirdPersonControls.leftMouse; + controls.mouseButtons.right = CONSTANTS.thirdPersonControls.rightMouse; + controls.mouseButtons.middle = CONSTANTS.thirdPersonControls.middleMouse; + controls.mouseButtons.wheel = CONSTANTS.thirdPersonControls.wheelMouse; + controls.unlockPointer(); + + const cameraDirection = new THREE.Vector3(); + camera.getWorldDirection(cameraDirection); + const targetOffset = cameraDirection.multiplyScalar(CONSTANTS.thirdPersonControls.targetOffset); + const targetPosition = new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z).add(targetOffset); + + controls.setPosition(camera.position.x, CONSTANTS.thirdPersonControls.cameraHeight, camera.position.z, true); + controls.setTarget(targetPosition.x, 0, targetPosition.z, true); + + controls.azimuthRotateSpeed = CONSTANTS.thirdPersonControls.azimuthRotateSpeed; + controls.polarRotateSpeed = CONSTANTS.thirdPersonControls.polarRotateSpeed; + controls.truckSpeed = CONSTANTS.thirdPersonControls.truckSpeed; + controls.minDistance = CONSTANTS.threeDimension.minDistance; + controls.maxDistance = CONSTANTS.thirdPersonControls.maxDistance; + controls.maxPolarAngle = CONSTANTS.thirdPersonControls.maxPolarAngle; +} \ No newline at end of file diff --git a/app/src/modules/scene/camera/switchView.tsx b/app/src/modules/scene/camera/switchView.tsx new file mode 100644 index 0000000..1241a30 --- /dev/null +++ b/app/src/modules/scene/camera/switchView.tsx @@ -0,0 +1,70 @@ +import * as THREE from "three"; +import { useEffect, useRef } from "react"; +import { useToggleView } from "../../../store/store"; +import { useThree } from "@react-three/fiber"; +import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi"; +import * as CONSTANTS from '../../../types/world/worldConstants'; + +export default function SwitchView() { + const { toggleView } = useToggleView(); + const state: any = useThree(); + const { set } = useThree(); + const perspectiveCamera = useRef(null); + const orthoCamera = useRef(null); + orthoCamera.current = new THREE.OrthographicCamera(-window.innerWidth / 2, window.innerWidth / 2, window.innerHeight / 2, -window.innerHeight / 2, 0.01, 1000); + perspectiveCamera.current = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 1000); + + useEffect(() => { + if (!perspectiveCamera.current || !orthoCamera.current) return; + if (toggleView) { + orthoCamera.current.zoom = 10; + orthoCamera.current.position.set(...CONSTANTS.twoDimension.defaultPosition); + orthoCamera.current.lookAt(new THREE.Vector3(...CONSTANTS.twoDimension.defaultTarget)); + orthoCamera.current.updateProjectionMatrix(); + set({ camera: orthoCamera.current }); + orthoCamera.current.updateProjectionMatrix(); + } else if (!toggleView) { + perspectiveCamera.current.position.set(...CONSTANTS.threeDimension.defaultPosition); + perspectiveCamera.current.lookAt(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget)); + set({ camera: perspectiveCamera.current }); + } + }, [toggleView, set]); + + useEffect(() => { + if (toggleView && state.controls) { + state.controls.mouseButtons.left = CONSTANTS.twoDimension.leftMouse; + state.controls.mouseButtons.right = CONSTANTS.twoDimension.rightMouse; + } else { + try { + const email = localStorage.getItem('email'); + const organization = (email!.split("@")[1]).split(".")[0]; + getCamera(organization, localStorage.getItem('userId')!).then((data) => { + if (data && data.position && data.target) { + state.controls?.setPosition(data.position.x, data.position.y, data.position.z); + state.controls?.setTarget(data.target.x, data.target.y, data.target.z); + localStorage.setItem("cameraPosition", JSON.stringify(data.position)); + localStorage.setItem("controlTarget", JSON.stringify(data.target)); + } else { + state.controls?.setPosition(...CONSTANTS.threeDimension.defaultPosition); + state.controls?.setTarget(...CONSTANTS.threeDimension.defaultTarget); + localStorage.setItem("cameraPosition", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultPosition))); + localStorage.setItem("controlTarget", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget))); + } + }); + } catch (error) { + console.error("Failed to retrieve camera position or target:", error); + state.controls?.setPosition(...CONSTANTS.threeDimension.defaultPosition); + state.controls?.setTarget(...CONSTANTS.threeDimension.defaultTarget); + } + + if (state.controls) { + state.controls.mouseButtons.left = CONSTANTS.threeDimension.leftMouse; + state.controls.mouseButtons.right = CONSTANTS.threeDimension.rightMouse; + } + } + }, [toggleView, state.controls]); + + return ( + <> + ); +} \ No newline at end of file diff --git a/app/src/modules/scene/camera/updateCameraPosition.ts b/app/src/modules/scene/camera/updateCameraPosition.ts new file mode 100644 index 0000000..4b76b1a --- /dev/null +++ b/app/src/modules/scene/camera/updateCameraPosition.ts @@ -0,0 +1,26 @@ +import { Socket } from "socket.io-client"; +import * as THREE from 'three'; + +export default function updateCamPosition( + controls: any, + socket: Socket, + position: THREE.Vector3, + rotation: THREE.Euler +) { + if (!controls.current) return; + const target = controls.current.getTarget(new THREE.Vector3()); + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + + const camData = { + organization: organization, + userId: localStorage.getItem("userId")!, + position: position, + target: new THREE.Vector3(target.x, 0, target.z), + rotation: new THREE.Vector3(rotation.x, rotation.y, rotation.z), + socketId: socket.id, + }; + socket.emit("v1:Camera:set", camData); + localStorage.setItem("cameraPosition", JSON.stringify(position)); + localStorage.setItem("controlTarget", JSON.stringify(new THREE.Vector3(target.x, 0, target.z))); +} \ No newline at end of file diff --git a/app/src/modules/scene/controls/controls.tsx b/app/src/modules/scene/controls/controls.tsx new file mode 100644 index 0000000..367098e --- /dev/null +++ b/app/src/modules/scene/controls/controls.tsx @@ -0,0 +1,136 @@ +import { CameraControls } from "@react-three/drei"; +import { useRef, useEffect } from "react"; +import { useThree } from "@react-three/fiber"; +import * as THREE from "three"; +import * as CONSTANTS from '../../../types/world/worldConstants'; + +import { useSocketStore, useToggleView, useResetCamera } from "../../../store/store"; +import { getCamera } from "../../../services/factoryBuilder/camera/getCameraApi"; +import updateCamPosition from "../camera/updateCameraPosition"; +import CamMode from "../camera/camMode"; +import SwitchView from "../camera/switchView"; + +export default function Controls() { + const controlsRef = useRef(null); + + const { toggleView } = useToggleView(); + const { resetCamera, setResetCamera } = useResetCamera(); + const { socket } = useSocketStore(); + const state = useThree(); + + useEffect(() => { + if (controlsRef.current) { + (controlsRef.current as any).mouseButtons.left = CONSTANTS.thirdPersonControls.leftMouse; + (controlsRef.current as any).mouseButtons.right = CONSTANTS.thirdPersonControls.rightMouse; + } + const email = localStorage.getItem("email"); + const organization = email!.split("@")[1].split(".")[0]; + getCamera(organization, localStorage.getItem("userId")!).then((data) => { + if (data && data.position && data.target) { + controlsRef.current?.setPosition(data.position.x, data.position.y, data.position.z); + controlsRef.current?.setTarget(data.target.x, data.target.y, data.target.z); + } else { + controlsRef.current?.setPosition(...CONSTANTS.threeDimension.defaultPosition); + controlsRef.current?.setTarget(...CONSTANTS.threeDimension.defaultTarget); + } + }) + .catch((error) => console.error("Failed to fetch camera data:", error)); + }, []); + + useEffect(() => { + if (resetCamera) { + controlsRef.current?.setPosition(...CONSTANTS.threeDimension.defaultPosition); + controlsRef.current?.setTarget(...CONSTANTS.threeDimension.defaultTarget); + controlsRef.current?.rotateAzimuthTo(CONSTANTS.threeDimension.defaultAzimuth); + + localStorage.setItem("cameraPosition", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultPosition))); + localStorage.setItem("controlTarget", JSON.stringify(new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget))); + + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + const camData = { + organization: organization, + userId: localStorage.getItem('userId')!, + position: new THREE.Vector3(...CONSTANTS.threeDimension.defaultPosition), + target: new THREE.Vector3(...CONSTANTS.threeDimension.defaultTarget), + rotation: new THREE.Vector3(...CONSTANTS.threeDimension.defaultRotation), + socketId: socket.id + }; + socket.emit('v1:Camera:set', camData) + + setResetCamera(false); + } + }, [resetCamera]); + + useEffect(() => { + controlsRef.current?.setBoundary(new THREE.Box3(new THREE.Vector3(...CONSTANTS.threeDimension.boundaryBottom), new THREE.Vector3(...CONSTANTS.threeDimension.boundaryTop))); + // state.scene.add(new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3(...CONSTANTS.threeDimension.boundaryBottom), new THREE.Vector3(...CONSTANTS.threeDimension.boundaryTop)), 0xffff00)); + let hasInteracted = false; + let intervalId: NodeJS.Timeout | null = null; + + const handleRest = () => { + if (hasInteracted && controlsRef.current && state.camera.position && !toggleView) { + const position = state.camera.position; + if (position.x === 0 && position.y === 0 && position.z === 0) return; + updateCamPosition(controlsRef, socket, position, state.camera.rotation); + stopInterval(); + } + }; + + const startInterval = () => { + hasInteracted = true; + if (!intervalId) { + intervalId = setInterval(() => { + if (controlsRef.current && !toggleView) { + handleRest(); + } + }, CONSTANTS.camPositionUpdateInterval); + } + }; + + const stopInterval = () => { + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + }; + + const controls = controlsRef.current; + if (controls) { + controls.addEventListener("sleep", handleRest); + controls.addEventListener("control", startInterval); + controls.addEventListener("controlend", stopInterval); + } + + return () => { + if (controls) { + controls.removeEventListener("sleep", handleRest); + controls.removeEventListener("control", startInterval); + controls.removeEventListener("controlend", stopInterval); + } + stopInterval(); + }; + }, [toggleView, state, socket]); + + return ( + <> + + + + + + ); +} \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/boundingBoxHelper.tsx b/app/src/modules/scene/controls/selection/boundingBoxHelper.tsx new file mode 100644 index 0000000..e725a7f --- /dev/null +++ b/app/src/modules/scene/controls/selection/boundingBoxHelper.tsx @@ -0,0 +1,62 @@ +import { Line } from "@react-three/drei"; +import { useMemo } from "react"; +import * as THREE from "three"; +import { useSelectedAssets } from "../../../../store/store"; + +const BoundingBox = ({ boundingBoxRef }: any) => { + const { selectedAssets } = useSelectedAssets(); + + const { points, boxProps } = useMemo(() => { + if (selectedAssets.length === 0) return { points: [], boxProps: {} }; + + const box = new THREE.Box3(); + selectedAssets.forEach((obj: any) => box.expandByObject(obj.clone())); + + const size = new THREE.Vector3(); + box.getSize(size); + const center = new THREE.Vector3(); + box.getCenter(center); + + const halfSize = size.clone().multiplyScalar(0.5); + const min = center.clone().sub(halfSize); + const max = center.clone().add(halfSize); + + const points: any = [ + [min.x, min.y, min.z], [max.x, min.y, min.z], + [max.x, min.y, min.z], [max.x, max.y, min.z], + [max.x, max.y, min.z], [min.x, max.y, min.z], + [min.x, max.y, min.z], [min.x, min.y, min.z], + + [min.x, min.y, max.z], [max.x, min.y, max.z], + [max.x, min.y, max.z], [max.x, max.y, max.z], + [max.x, max.y, max.z], [min.x, max.y, max.z], + [min.x, max.y, max.z], [min.x, min.y, max.z], + + [min.x, min.y, min.z], [min.x, min.y, max.z], + [max.x, min.y, min.z], [max.x, min.y, max.z], + [max.x, max.y, min.z], [max.x, max.y, max.z], + [min.x, max.y, min.z], [min.x, max.y, max.z], + ]; + + return { + points, + boxProps: { position: center.toArray(), args: size.toArray() } + }; + }, [selectedAssets]); + + return ( + <> + {points.length > 0 && ( + <> + + + + + + + )} + + ); +}; + +export default BoundingBox; diff --git a/app/src/modules/scene/controls/selection/copyPasteControls.tsx b/app/src/modules/scene/controls/selection/copyPasteControls.tsx new file mode 100644 index 0000000..3eafd93 --- /dev/null +++ b/app/src/modules/scene/controls/selection/copyPasteControls.tsx @@ -0,0 +1,209 @@ +import * as THREE from "three"; +import { useEffect, useMemo } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; +import { toast } from "react-toastify"; +// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; +import * as Types from "../../../../types/world/worldTypes"; + +const CopyPasteControls = ({ itemsGroupRef, copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, selectionGroup, setDuplicatedObjects, movedObjects, setMovedObjects, rotatedObjects, setRotatedObjects, boundingBoxRef }: any) => { + const { camera, controls, gl, scene, pointer, raycaster } = useThree(); + const { toggleView } = useToggleView(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); + const { floorItems, setFloorItems } = useFloorItems(); + const { socket } = useSocketStore() + + useEffect(() => { + if (!camera || !scene || toggleView) return; + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; + + let isMoving = false; + + const onPointerDown = () => { + isMoving = false; + }; + + const onPointerMove = () => { + isMoving = true; + }; + + const onPointerUp = (event: PointerEvent) => { + if (!isMoving && pastedObjects.length > 0 && event.button === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + event.preventDefault(); + addPastedObjects(); + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.ctrlKey && event.key.toLowerCase() === "c" && movedObjects.length === 0 && rotatedObjects.length === 0) { + copySelection(); + } + if (event.ctrlKey && event.key.toLowerCase() === "v" && copiedObjects.length > 0 && pastedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + pasteCopiedObjects(); + } + }; + + if (!toggleView) { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("keydown", onKeyDown); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + }; + + }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, movedObjects, socket, floorItems, rotatedObjects]); + + useFrame(() => { + if (pastedObjects.length > 0) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (point) { + const position = new THREE.Vector3(); + if (boundingBoxRef.current) { + boundingBoxRef.current?.getWorldPosition(position) + selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z)); + } else { + const box = new THREE.Box3(); + pastedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj.clone())); + const center = new THREE.Vector3(); + box.getCenter(center); + selectionGroup.current.position.set(point.x - (center.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (center.z - selectionGroup.current.position.z)); + } + } + } + }); + + const copySelection = () => { + if (selectedAssets.length > 0) { + const newClones = selectedAssets.map((asset: any) => { + const clone = asset.clone(); + clone.position.copy(asset.position); + return clone; + }); + setCopiedObjects(newClones); + toast.info("Objects copied!"); + } + }; + + const pasteCopiedObjects = () => { + if (copiedObjects.length > 0 && pastedObjects.length === 0) { + const newClones = copiedObjects.map((obj: THREE.Object3D) => { + const clone = obj.clone(); + clone.position.copy(obj.position); + return clone; + }); + selectionGroup.current.add(...newClones); + setpastedObjects([...newClones]); + setSelectedAssets([...newClones]); + + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (point) { + const position = new THREE.Vector3(); + if (boundingBoxRef.current) { + boundingBoxRef.current?.getWorldPosition(position) + selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z)); + } else { + const box = new THREE.Box3(); + newClones.forEach((obj: THREE.Object3D) => box.expandByObject(obj.clone())); + const center = new THREE.Vector3(); + box.getCenter(center); + selectionGroup.current.position.set(point.x - (center.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (center.z - selectionGroup.current.position.z)); + } + } + } + }; + + const addPastedObjects = () => { + if (pastedObjects.length === 0) return; + pastedObjects.forEach(async (obj: THREE.Object3D) => { + const worldPosition = new THREE.Vector3(); + obj.getWorldPosition(worldPosition); + obj.position.copy(worldPosition); + + if (itemsGroupRef.current) { + + const newFloorItem: Types.FloorItemType = { + modeluuid: obj.uuid, + modelname: obj.userData.name, + modelfileID: obj.userData.modelId, + position: [worldPosition.x, worldPosition.y, worldPosition.z], + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, }, + isLocked: false, + isVisible: true + }; + + setFloorItems((prevItems: Types.FloorItems) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : "default"; + + //REST + + // await setFloorItemApi( + // organization, + // obj.uuid, + // obj.userData.name, + // [worldPosition.x, worldPosition.y, worldPosition.z], + // { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z }, + // obj.userData.modelId, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modeluuid: newFloorItem.modeluuid, + modelname: newFloorItem.modelname, + modelfileID: newFloorItem.modelfileID, + position: newFloorItem.position, + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + }; + + socket.emit("v1:FloorItems:set", data); + + itemsGroupRef.current.add(obj); + } + }); + + toast.success("Object added!"); + clearSelection(); + }; + + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setMovedObjects([]); + setpastedObjects([]); + setDuplicatedObjects([]); + setRotatedObjects([]); + setSelectedAssets([]); + } + + return ( + <> + ); +}; + +export default CopyPasteControls; \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/duplicationControls.tsx b/app/src/modules/scene/controls/selection/duplicationControls.tsx new file mode 100644 index 0000000..a38b6cd --- /dev/null +++ b/app/src/modules/scene/controls/selection/duplicationControls.tsx @@ -0,0 +1,190 @@ +import * as THREE from "three"; +import { useEffect, useMemo } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; +import { toast } from "react-toastify"; +// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; +import * as Types from "../../../../types/world/worldTypes"; + +const DuplicationControls = ({ itemsGroupRef, duplicatedObjects, setDuplicatedObjects, setpastedObjects, selectionGroup, movedObjects, setMovedObjects, rotatedObjects, setRotatedObjects, boundingBoxRef }: any) => { + const { camera, controls, gl, scene, pointer, raycaster } = useThree(); + const { toggleView } = useToggleView(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); + const { floorItems, setFloorItems } = useFloorItems(); + const { socket } = useSocketStore(); + + + useEffect(() => { + if (!camera || !scene || toggleView) return; + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; + + let isMoving = false; + + const onPointerDown = () => { + isMoving = false; + }; + + const onPointerMove = () => { + isMoving = true; + }; + + const onPointerUp = (event: PointerEvent) => { + if (!isMoving && duplicatedObjects.length > 0 && event.button === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + event.preventDefault(); + addDuplicatedAssets(); + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key.toLowerCase() === "d") { + event.preventDefault(); + if (event.ctrlKey && event.key.toLowerCase() === "d" && selectedAssets.length > 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + duplicateSelection(); + } + } + }; + + if (!toggleView) { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("keydown", onKeyDown); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + }; + + }, [camera, controls, scene, toggleView, selectedAssets, duplicatedObjects, movedObjects, socket, floorItems, rotatedObjects]); + + useFrame(() => { + if (duplicatedObjects.length > 0) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + if (point) { + const position = new THREE.Vector3(); + if (boundingBoxRef.current) { + boundingBoxRef.current?.getWorldPosition(position) + selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z)); + } else { + const box = new THREE.Box3(); + duplicatedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj.clone())); + const center = new THREE.Vector3(); + box.getCenter(center); + selectionGroup.current.position.set(point.x - (center.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (center.z - selectionGroup.current.position.z)); + } + } + } + }); + + const duplicateSelection = () => { + if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { + const newClones = selectedAssets.map((asset: any) => { + const clone = asset.clone(); + clone.position.copy(asset.position); + return clone; + }); + + selectionGroup.current.add(...newClones); + setDuplicatedObjects(newClones); + + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (point) { + const position = new THREE.Vector3(); + boundingBoxRef.current?.getWorldPosition(position) + selectionGroup.current.position.set(point.x - (position.x - selectionGroup.current.position.x), selectionGroup.current.position.y, point.z - (position.z - selectionGroup.current.position.z)); + } + } + }; + + const addDuplicatedAssets = () => { + if (duplicatedObjects.length === 0) return; + duplicatedObjects.forEach(async (obj: THREE.Object3D) => { + const worldPosition = new THREE.Vector3(); + obj.getWorldPosition(worldPosition); + obj.position.copy(worldPosition); + + if (itemsGroupRef.current) { + + const newFloorItem: Types.FloorItemType = { + modeluuid: obj.uuid, + modelname: obj.userData.name, + modelfileID: obj.userData.modelId, + position: [worldPosition.x, worldPosition.y, worldPosition.z], + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, }, + isLocked: false, + isVisible: true + }; + + setFloorItems((prevItems: Types.FloorItems) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : "default"; + + //REST + + // await setFloorItemApi( + // organization, + // obj.uuid, + // obj.userData.name, + // [worldPosition.x, worldPosition.y, worldPosition.z], + // { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z }, + // obj.userData.modelId, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modeluuid: newFloorItem.modeluuid, + modelname: newFloorItem.modelname, + modelfileID: newFloorItem.modelfileID, + position: newFloorItem.position, + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + }; + + socket.emit("v1:FloorItems:set", data); + + itemsGroupRef.current.add(obj); + } + }); + + toast.success("Object duplicated!"); + clearSelection(); + } + + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setMovedObjects([]); + setpastedObjects([]); + setDuplicatedObjects([]); + setRotatedObjects([]); + setSelectedAssets([]); + } + + return ( + <> + ); +}; + +export default DuplicationControls; \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/moveControls.tsx b/app/src/modules/scene/controls/selection/moveControls.tsx new file mode 100644 index 0000000..5f7eb11 --- /dev/null +++ b/app/src/modules/scene/controls/selection/moveControls.tsx @@ -0,0 +1,239 @@ +import * as THREE from "three"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; +// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; +import { toast } from "react-toastify"; +import * as Types from "../../../../types/world/worldTypes"; + +function MoveControls({ movedObjects, setMovedObjects, itemsGroupRef, copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, duplicatedObjects, setDuplicatedObjects, selectionGroup, rotatedObjects, setRotatedObjects, boundingBoxRef }: any) { + const { camera, controls, gl, scene, pointer, raycaster } = useThree(); + const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); + + const { toggleView } = useToggleView(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const { floorItems, setFloorItems } = useFloorItems(); + const { socket } = useSocketStore(); + const itemsData = useRef([]); + + useEffect(() => { + if (!camera || !scene || toggleView || !itemsGroupRef.current) return; + + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; + + let isMoving = false; + + const onPointerDown = () => { + isMoving = false; + }; + + const onPointerMove = () => { + isMoving = true; + }; + + const onPointerUp = (event: PointerEvent) => { + if (!isMoving && movedObjects.length > 0 && event.button === 0) { + event.preventDefault(); + placeMovedAssets(); + } + if (!isMoving && movedObjects.length > 0 && event.button === 2) { + event.preventDefault(); + + clearSelection(); + movedObjects.forEach((asset: any) => { + if (itemsGroupRef.current) { + itemsGroupRef.current.attach(asset); + } + }); + + setFloorItems([...floorItems, ...itemsData.current]); + + setMovedObjects([]); + itemsData.current = []; + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || rotatedObjects.length > 0) return; + if (event.key.toLowerCase() === "g") { + if (selectedAssets.length > 0) { + moveAssets(); + itemsData.current = floorItems.filter((item: { modeluuid: string }) => selectedAssets.some((asset: any) => asset.uuid === item.modeluuid)); + } + } + if (event.key.toLowerCase() === "escape") { + event.preventDefault(); + + clearSelection(); + movedObjects.forEach((asset: any) => { + if (itemsGroupRef.current) { + itemsGroupRef.current.attach(asset); + } + }); + + setFloorItems([...floorItems, ...itemsData.current]); + + setMovedObjects([]); + itemsData.current = []; + } + }; + + if (!toggleView) { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("keydown", onKeyDown); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + }; + }, [camera, controls, scene, toggleView, selectedAssets, socket, floorItems, pastedObjects, duplicatedObjects, movedObjects, rotatedObjects]); + + const gridSize = 0.25; + const moveSpeed = 0.25; + const isGridSnap = false; + + useFrame(() => { + if (movedObjects.length > 0) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (point) { + let targetX = point.x; + let targetZ = point.z; + + if (isGridSnap) { + targetX = Math.round(point.x / gridSize) * gridSize; + targetZ = Math.round(point.z / gridSize) * gridSize; + } + + const position = new THREE.Vector3(); + if (boundingBoxRef.current) { + boundingBoxRef.current.getWorldPosition(position); + selectionGroup.current.position.lerp( + new THREE.Vector3( + targetX - (position.x - selectionGroup.current.position.x), + selectionGroup.current.position.y, + targetZ - (position.z - selectionGroup.current.position.z) + ), + moveSpeed + ); + } else { + const box = new THREE.Box3(); + movedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj)); + const center = new THREE.Vector3(); + box.getCenter(center); + + selectionGroup.current.position.lerp( + new THREE.Vector3( + targetX - (center.x - selectionGroup.current.position.x), + selectionGroup.current.position.y, + targetZ - (center.z - selectionGroup.current.position.z) + ), + moveSpeed + ); + } + } + } + }); + + + const moveAssets = () => { + const updatedItems = floorItems.filter((item: { modeluuid: string }) => !selectedAssets.some((asset: any) => asset.uuid === item.modeluuid)); + setFloorItems(updatedItems); + setMovedObjects(selectedAssets); + selectedAssets.forEach((asset: any) => { selectionGroup.current.attach(asset); }); + } + + const placeMovedAssets = () => { + if (movedObjects.length === 0) return; + + movedObjects.forEach(async (obj: THREE.Object3D) => { + const worldPosition = new THREE.Vector3(); + obj.getWorldPosition(worldPosition); + + selectionGroup.current.remove(obj); + obj.position.copy(worldPosition); + + if (itemsGroupRef.current) { + + const newFloorItem: Types.FloorItemType = { + modeluuid: obj.uuid, + modelname: obj.userData.name, + modelfileID: obj.userData.modelId, + position: [worldPosition.x, worldPosition.y, worldPosition.z], + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, }, + isLocked: false, + isVisible: true + }; + + setFloorItems((prevItems: Types.FloorItems) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : "default"; + + //REST + + // await setFloorItemApi( + // organization, + // obj.uuid, + // obj.userData.name, + // [worldPosition.x, worldPosition.y, worldPosition.z], + // { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z }, + // obj.userData.modelId, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modeluuid: newFloorItem.modeluuid, + modelname: newFloorItem.modelname, + modelfileID: newFloorItem.modelfileID, + position: newFloorItem.position, + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + }; + + socket.emit("v1:FloorItems:set", data); + + itemsGroupRef.current.add(obj); + } + }); + toast.success("Object moved!"); + + itemsData.current = []; + clearSelection(); + } + + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setpastedObjects([]); + setDuplicatedObjects([]); + setMovedObjects([]); + setRotatedObjects([]); + setSelectedAssets([]); + } + + return ( + <> + ) +} + +export default MoveControls \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/rotateControls.tsx b/app/src/modules/scene/controls/selection/rotateControls.tsx new file mode 100644 index 0000000..b28f921 --- /dev/null +++ b/app/src/modules/scene/controls/selection/rotateControls.tsx @@ -0,0 +1,242 @@ +import * as THREE from "three"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; +// import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; +import { toast } from "react-toastify"; +import * as Types from "../../../../types/world/worldTypes"; + +function RotateControls({ rotatedObjects, setRotatedObjects, movedObjects, setMovedObjects, itemsGroupRef, copiedObjects, setCopiedObjects, pastedObjects, setpastedObjects, duplicatedObjects, setDuplicatedObjects, selectionGroup, boundingBoxRef }: any) { + const { camera, controls, gl, scene, pointer, raycaster } = useThree(); + const plane = useMemo(() => new THREE.Plane(new THREE.Vector3(0, 1, 0), 0), []); + + const { toggleView } = useToggleView(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const { floorItems, setFloorItems } = useFloorItems(); + const { socket } = useSocketStore(); + const itemsData = useRef([]); + + const prevPointerPosition = useRef(null); + + useEffect(() => { + if (!camera || !scene || toggleView || !itemsGroupRef.current) return; + + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; + + let isMoving = false; + + const onPointerDown = () => { + isMoving = false; + }; + + const onPointerMove = () => { + isMoving = true; + }; + + const onPointerUp = (event: PointerEvent) => { + if (!isMoving && rotatedObjects.length > 0 && event.button === 0) { + event.preventDefault(); + placeRotatedAssets(); + } + if (!isMoving && rotatedObjects.length > 0 && event.button === 2) { + event.preventDefault(); + + clearSelection(); + rotatedObjects.forEach((asset: any) => { + if (itemsGroupRef.current) { + itemsGroupRef.current.attach(asset); + } + }); + + setFloorItems([...floorItems, ...itemsData.current]); + + setRotatedObjects([]); + itemsData.current = []; + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (pastedObjects.length > 0 || duplicatedObjects.length > 0 || movedObjects.length > 0) return; + if (event.key.toLowerCase() === "r") { + if (selectedAssets.length > 0) { + rotateAssets(); + itemsData.current = floorItems.filter((item: { modeluuid: string }) => selectedAssets.some((asset: any) => asset.uuid === item.modeluuid)); + } + } + if (event.key.toLowerCase() === "escape") { + event.preventDefault(); + + clearSelection(); + rotatedObjects.forEach((asset: any) => { + if (itemsGroupRef.current) { + itemsGroupRef.current.attach(asset); + } + }); + + setFloorItems([...floorItems, ...itemsData.current]); + + setRotatedObjects([]); + itemsData.current = []; + } + }; + + if (!toggleView) { + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("keydown", onKeyDown); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + }; + }, [camera, controls, scene, toggleView, selectedAssets, socket, floorItems, pastedObjects, duplicatedObjects, rotatedObjects, movedObjects]); + + useFrame(() => { + if (rotatedObjects.length > 0) { + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (point && prevPointerPosition.current) { + const box = new THREE.Box3(); + rotatedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj)); + const center = new THREE.Vector3(); + box.getCenter(center); + + const delta = new THREE.Vector3().subVectors(point, center); + const prevPointerPosition3D = new THREE.Vector3(prevPointerPosition.current.x, 0, prevPointerPosition.current.y); + + const angle = Math.atan2(delta.z, delta.x) - Math.atan2(prevPointerPosition3D.z - center.z, prevPointerPosition3D.x - center.x); + + selectionGroup.current.rotation.y += -angle; + + selectionGroup.current.position.sub(center); + selectionGroup.current.position.applyAxisAngle(new THREE.Vector3(0, 1, 0), -angle); + selectionGroup.current.position.add(center); + + prevPointerPosition.current = new THREE.Vector2(point.x, point.z); + } + } + }); + + const rotateAssets = () => { + const updatedItems = floorItems.filter((item: { modeluuid: string }) => !selectedAssets.some((asset: any) => asset.uuid === item.modeluuid)); + setFloorItems(updatedItems); + + const box = new THREE.Box3(); + selectedAssets.forEach((asset: any) => box.expandByObject(asset)); + const center = new THREE.Vector3(); + box.getCenter(center); + + const intersectionPoint = new THREE.Vector3(); + raycaster.setFromCamera(pointer, camera); + const point = raycaster.ray.intersectPlane(plane, intersectionPoint); + + if (point) { + prevPointerPosition.current = new THREE.Vector2(point.x, point.z); + } + + selectedAssets.forEach((asset: any) => { + selectionGroup.current.attach(asset); + }); + + setRotatedObjects(selectedAssets); + }; + + const placeRotatedAssets = () => { + if (rotatedObjects.length === 0) return; + + rotatedObjects.forEach(async (obj: THREE.Object3D) => { + const worldPosition = new THREE.Vector3(); + const worldQuaternion = new THREE.Quaternion(); + + obj.getWorldPosition(worldPosition); + obj.getWorldQuaternion(worldQuaternion); + + selectionGroup.current.remove(obj); + + obj.position.copy(worldPosition); + obj.quaternion.copy(worldQuaternion); + + + if (itemsGroupRef.current) { + + const newFloorItem: Types.FloorItemType = { + modeluuid: obj.uuid, + modelname: obj.userData.name, + modelfileID: obj.userData.modelId, + position: [worldPosition.x, worldPosition.y, worldPosition.z], + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z, }, + isLocked: false, + isVisible: true + }; + + setFloorItems((prevItems: Types.FloorItems) => { + const updatedItems = [...(prevItems || []), newFloorItem]; + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + + const email = localStorage.getItem("email"); + const organization = email ? email.split("@")[1].split(".")[0] : "default"; + + //REST + + // await setFloorItemApi( + // organization, + // obj.uuid, + // obj.userData.name, + // [worldPosition.x, worldPosition.y, worldPosition.z], + // { "x": obj.rotation.x, "y": obj.rotation.y, "z": obj.rotation.z }, + // obj.userData.modelId, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization, + modeluuid: newFloorItem.modeluuid, + modelname: newFloorItem.modelname, + modelfileID: newFloorItem.modelfileID, + position: newFloorItem.position, + rotation: { x: obj.rotation.x, y: obj.rotation.y, z: obj.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id, + }; + + socket.emit("v1:FloorItems:set", data); + + itemsGroupRef.current.add(obj); + } + }); + toast.success("Object rotated!"); + + itemsData.current = []; + clearSelection(); + } + + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setpastedObjects([]); + setDuplicatedObjects([]); + setMovedObjects([]); + setRotatedObjects([]); + setSelectedAssets([]); + } + + return ( + <> + ) +} + +export default RotateControls \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/selectionControls.tsx b/app/src/modules/scene/controls/selection/selectionControls.tsx new file mode 100644 index 0000000..4f62c50 --- /dev/null +++ b/app/src/modules/scene/controls/selection/selectionControls.tsx @@ -0,0 +1,237 @@ +import * as THREE from "three"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { SelectionBox } from "three/examples/jsm/interactive/SelectionBox"; +import { SelectionHelper } from "./selectionHelper"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useFloorItems, useSelectedAssets, useSocketStore, useToggleView } from "../../../../store/store"; +import BoundingBox from "./boundingBoxHelper"; +import { toast } from "react-toastify"; +// import { deleteFloorItem } from '../../../../services/factoryBuilder/assest/floorAsset/deleteFloorItemApi'; +import * as Types from "../../../../types/world/worldTypes"; + +import DuplicationControls from "./duplicationControls"; +import CopyPasteControls from "./copyPasteControls"; +import MoveControls from "./moveControls"; +import RotateControls from "./rotateControls"; + +const SelectionControls: React.FC = () => { + const { camera, controls, gl, scene, pointer } = useThree(); + const itemsGroupRef = useRef(undefined); + const selectionGroup = useRef() as Types.RefGroup; + const { toggleView } = useToggleView(); + const { selectedAssets, setSelectedAssets } = useSelectedAssets(); + const [movedObjects, setMovedObjects] = useState([]); + const [rotatedObjects, setRotatedObjects] = useState([]); + const [copiedObjects, setCopiedObjects] = useState([]); + const [pastedObjects, setpastedObjects] = useState([]); + const [duplicatedObjects, setDuplicatedObjects] = useState([]); + const boundingBoxRef = useRef(); + const { floorItems, setFloorItems } = useFloorItems(); + const { socket } = useSocketStore(); + const selectionBox = useMemo(() => new SelectionBox(camera, scene), [camera, scene]); + + useEffect(() => { + if (!camera || !scene || toggleView) return; + + const canvasElement = gl.domElement; + canvasElement.tabIndex = 0; + + const itemsGroup: any = scene.getObjectByName("itemsGroup"); + itemsGroupRef.current = itemsGroup; + + let isSelecting = false; + let isCtrlSelecting = false; + + const helper = new SelectionHelper(gl); + + if (!itemsGroup) { + toast.warn("itemsGroup not found in the scene."); + return; + } + + const onPointerDown = (event: PointerEvent) => { + if (event.button !== 0) return + isSelecting = false; + isCtrlSelecting = event.ctrlKey; + if (event.ctrlKey && duplicatedObjects.length === 0) { + if (controls) (controls as any).enabled = false; + selectionBox.startPoint.set(pointer.x, pointer.y, 0); + } + }; + + const onPointerMove = (event: PointerEvent) => { + isSelecting = true; + if (helper.isDown && event.ctrlKey && duplicatedObjects.length === 0 && isCtrlSelecting) { + selectionBox.endPoint.set(pointer.x, pointer.y, 0); + } + }; + + const onPointerUp = (event: PointerEvent) => { + if (isSelecting && isCtrlSelecting) { + isCtrlSelecting = false; + isSelecting = false; + if (event.ctrlKey && duplicatedObjects.length === 0) { + selectAssets(); + } + } else if (!isSelecting && selectedAssets.length > 0 && ((pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) || event.button !== 0)) { + clearSelection(); + helper.enabled = true; + isCtrlSelecting = false; + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (movedObjects.length > 0 || rotatedObjects.length > 0) return; + if (event.key.toLowerCase() === "escape") { + event.preventDefault(); + clearSelection(); + } + if (event.key.toLowerCase() === "delete") { + event.preventDefault(); + deleteSelection(); + } + }; + + const onContextMenu = (event: MouseEvent) => { + event.preventDefault(); + clearSelection(); + } + + if (!toggleView) { + helper.enabled = true; + canvasElement.addEventListener("pointerdown", onPointerDown); + canvasElement.addEventListener("pointermove", onPointerMove); + canvasElement.addEventListener("contextmenu", onContextMenu); + canvasElement.addEventListener("pointerup", onPointerUp); + canvasElement.addEventListener("keydown", onKeyDown); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onPointerDown); + canvasElement.removeEventListener("pointermove", onPointerMove); + canvasElement.removeEventListener("contextmenu", onContextMenu); + canvasElement.removeEventListener("pointerup", onPointerUp); + canvasElement.removeEventListener("keydown", onKeyDown); + helper.enabled = false; + helper.dispose(); + }; + }, [camera, controls, scene, toggleView, selectedAssets, copiedObjects, pastedObjects, duplicatedObjects, movedObjects, socket, floorItems, rotatedObjects]); + + useFrame(() => { + if (pastedObjects.length === 0 && duplicatedObjects.length === 0 && movedObjects.length === 0 && rotatedObjects.length === 0) { + selectionGroup.current.position.set(0, 0, 0); + } + }); + + const selectAssets = () => { + selectionBox.endPoint.set(pointer.x, pointer.y, 0); + if (controls) (controls as any).enabled = true; + + let selectedObjects = selectionBox.select(); + let Objects = new Set(); + + selectedObjects.map((object) => { + let currentObject: THREE.Object3D | null = object; + while (currentObject) { + if (currentObject.userData.modelId) { + Objects.add(currentObject); + break; + } + currentObject = currentObject.parent || null; + } + }) + + if (Objects.size === 0) { + clearSelection(); + return; + } + + const updatedSelections = new Set(selectedAssets); + Objects.forEach((obj) => { + updatedSelections.has(obj) ? updatedSelections.delete(obj) : updatedSelections.add(obj); + }); + + const selected = Array.from(updatedSelections); + + setSelectedAssets(selected); + }; + + const clearSelection = () => { + selectionGroup.current.children = []; + selectionGroup.current.position.set(0, 0, 0); + selectionGroup.current.rotation.set(0, 0, 0); + setpastedObjects([]); + setDuplicatedObjects([]); + setSelectedAssets([]); + } + + const deleteSelection = () => { + if (selectedAssets.length > 0 && duplicatedObjects.length === 0) { + const email = localStorage.getItem('email'); + const organization = (email!.split("@")[1]).split(".")[0]; + + const storedItems = JSON.parse(localStorage.getItem("FloorItems") || '[]'); + const selectedUUIDs = selectedAssets.map((mesh: THREE.Object3D) => mesh.uuid); + + const updatedStoredItems = storedItems.filter((item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid)); + localStorage.setItem("FloorItems", JSON.stringify(updatedStoredItems)); + + selectedAssets.forEach((selectedMesh: THREE.Object3D) => { + + //REST + + // const response = await deleteFloorItem(organization, selectedMesh.uuid, selectedMesh.userData.name); + + //SOCKET + + const data = { + organization: organization, + modeluuid: selectedMesh.uuid, + modelname: selectedMesh.userData.name, + socketId: socket.id + }; + + socket.emit('v1:FloorItems:delete', data); + + selectedMesh.traverse((child: THREE.Object3D) => { + if (child instanceof THREE.Mesh) { + if (child.geometry) child.geometry.dispose(); + if (Array.isArray(child.material)) { + child.material.forEach((material) => { + if (material.map) material.map.dispose(); + material.dispose(); + }); + } else if (child.material) { + if (child.material.map) child.material.map.dispose(); + child.material.dispose(); + } + } + }); + + itemsGroupRef.current?.remove(selectedMesh); + }); + + const updatedItems = floorItems.filter((item: { modeluuid: string }) => !selectedUUIDs.includes(item.modeluuid)); + setFloorItems(updatedItems); + + } + toast.success("Selected models removed!"); + clearSelection(); + }; + + return ( + <> + + + + + + + + + + + ); +}; + +export default SelectionControls; \ No newline at end of file diff --git a/app/src/modules/scene/controls/selection/selectionHelper.ts b/app/src/modules/scene/controls/selection/selectionHelper.ts new file mode 100644 index 0000000..c1acaf6 --- /dev/null +++ b/app/src/modules/scene/controls/selection/selectionHelper.ts @@ -0,0 +1,115 @@ +import { Vector2, WebGLRenderer } from 'three'; + +class SelectionHelper { + element: HTMLDivElement; + renderer: WebGLRenderer; + startPoint: Vector2; + pointTopLeft: Vector2; + pointBottomRight: Vector2; + isDown: boolean; + enabled: boolean; + + constructor(renderer: WebGLRenderer) { + this.element = document.createElement('div'); + this.element.style.position = 'fixed'; + this.element.style.border = '1px solid #55aaff'; + this.element.style.backgroundColor = 'rgba(75, 160, 255, 0.3)'; + this.element.style.pointerEvents = 'none'; + this.element.style.display = 'none'; + + this.renderer = renderer; + + this.startPoint = new Vector2(); + this.pointTopLeft = new Vector2(); + this.pointBottomRight = new Vector2(); + + this.isDown = false; + this.enabled = true; + + this.onPointerDown = this.onPointerDown.bind(this); + this.onPointerMove = this.onPointerMove.bind(this); + this.onPointerUp = this.onPointerUp.bind(this); + + this.renderer.domElement.addEventListener('pointerdown', this.onPointerDown); + this.renderer.domElement.addEventListener('pointermove', this.onPointerMove); + this.renderer.domElement.addEventListener('pointerup', this.onPointerUp); + window.addEventListener("blur", this.cleanup.bind(this)); + } + + dispose() { + this.enabled = false; + this.isDown = false; + this.cleanup(); + + this.renderer.domElement.removeEventListener("pointerdown", this.onPointerDown); + this.renderer.domElement.removeEventListener("pointermove", this.onPointerMove); + this.renderer.domElement.removeEventListener("pointerup", this.onPointerUp); + window.removeEventListener("blur", this.cleanup); + } + + private cleanup() { + this.isDown = false; + this.element.style.display = 'none'; + if (this.element.parentElement) { + this.element.parentElement.removeChild(this.element); + } + } + + onPointerDown(event: PointerEvent) { + if (!this.enabled || !event.ctrlKey || event.button !== 0) return; + + this.isDown = true; + this.onSelectStart(event); + } + + onPointerMove(event: PointerEvent) { + if (!this.enabled || !this.isDown || !event.ctrlKey) return; + + this.onSelectMove(event); + } + + onPointerUp() { + if (!this.enabled) return; + + this.isDown = false; + this.onSelectOver(); + } + + onSelectStart(event: PointerEvent) { + this.element.style.display = 'none'; + this.renderer.domElement.parentElement?.appendChild(this.element); + + this.element.style.left = `${event.clientX}px`; + this.element.style.top = `${event.clientY}px`; + this.element.style.width = '0px'; + this.element.style.height = '0px'; + + this.startPoint.x = event.clientX; + this.startPoint.y = event.clientY; + } + + onSelectMove(event: PointerEvent) { + if (!this.isDown) return; + + this.element.style.display = 'block'; + + this.pointBottomRight.x = Math.max(this.startPoint.x, event.clientX); + this.pointBottomRight.y = Math.max(this.startPoint.y, event.clientY); + this.pointTopLeft.x = Math.min(this.startPoint.x, event.clientX); + this.pointTopLeft.y = Math.min(this.startPoint.y, event.clientY); + + this.element.style.left = `${this.pointTopLeft.x}px`; + this.element.style.top = `${this.pointTopLeft.y}px`; + this.element.style.width = `${this.pointBottomRight.x - this.pointTopLeft.x}px`; + this.element.style.height = `${this.pointBottomRight.y - this.pointTopLeft.y}px`; + } + + onSelectOver() { + this.element.style.display = 'none'; + if (this.element.parentElement) { + this.element.parentElement.removeChild(this.element); + } + } +} + +export { SelectionHelper }; \ No newline at end of file diff --git a/app/src/modules/scene/controls/transformControls.tsx b/app/src/modules/scene/controls/transformControls.tsx new file mode 100644 index 0000000..5e8c213 --- /dev/null +++ b/app/src/modules/scene/controls/transformControls.tsx @@ -0,0 +1,120 @@ +import { TransformControls } from "@react-three/drei"; +import * as THREE from "three"; +import { useselectedFloorItem, useObjectPosition, useObjectScale, useObjectRotation, useTransformMode, useFloorItems, useSocketStore, useActiveTool } from "../../../store/store"; +import { useThree } from "@react-three/fiber"; + +import * as Types from '../../../types/world/worldTypes'; +import { useEffect } from "react"; + +export default function TransformControl() { + const state = useThree(); + const { selectedFloorItem, setselectedFloorItem } = useselectedFloorItem(); + const { objectPosition, setObjectPosition } = useObjectPosition(); + const { objectScale, setObjectScale } = useObjectScale(); + const { objectRotation, setObjectRotation } = useObjectRotation(); + const { transformMode, setTransformMode } = useTransformMode(); + const { floorItems, setFloorItems } = useFloorItems(); + const { activeTool, setActiveTool } = useActiveTool(); + const { socket } = useSocketStore(); + + function handleObjectChange() { + if (selectedFloorItem && transformMode) { + setObjectPosition(selectedFloorItem.position); + setObjectScale(selectedFloorItem.scale); + setObjectRotation({ + x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x), + y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y), + z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z), + }); + } + } + function handleMouseUp() { + if (selectedFloorItem) { + setObjectPosition(selectedFloorItem.position); + setObjectScale(selectedFloorItem.scale); + setObjectRotation({ + x: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.x), + y: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.y), + z: THREE.MathUtils.radToDeg(selectedFloorItem.rotation.z), + }); + } + setFloorItems((prevItems: Types.FloorItems) => { + if (!prevItems) { + return + } + let updatedItem: any = null; + const updatedItems = prevItems.map((item) => { + if (item.modeluuid === selectedFloorItem?.uuid) { + updatedItem = { + ...item, + position: [selectedFloorItem.position.x, selectedFloorItem.position.y, selectedFloorItem.position.z,] as [number, number, number], + rotation: { x: selectedFloorItem.rotation.x, y: selectedFloorItem.rotation.y, z: selectedFloorItem.rotation.z, }, + }; + return updatedItem; + } + return item; + }); + if (updatedItem && selectedFloorItem) { + const email = localStorage.getItem('email') + const organization = (email!.split("@")[1]).split(".")[0]; + + //REST + + // setFloorItemApi( + // organization, + // updatedItem.modeluuid, + // updatedItem.modelname, + // [selectedFloorItem.position.x, selectedFloorItem.position.y, selectedFloorItem.position.z,], + // { "x": selectedFloorItem.rotation.x, "y": selectedFloorItem.rotation.y, "z": selectedFloorItem.rotation.z }, + // false, + // true, + // ); + + //SOCKET + + const data = { + organization: organization, + modeluuid: updatedItem.modeluuid, + modelname: updatedItem.modelname, + position: [selectedFloorItem.position.x, selectedFloorItem.position.y, selectedFloorItem.position.z], + rotation: { "x": selectedFloorItem.rotation.x, "y": selectedFloorItem.rotation.y, "z": selectedFloorItem.rotation.z }, + isLocked: false, + isVisible: true, + socketId: socket.id + } + + socket.emit('v1:FloorItems:set', data); + } + localStorage.setItem("FloorItems", JSON.stringify(updatedItems)); + return updatedItems; + }); + } + + useEffect(() => { + if (activeTool === "Add pillar" || activeTool === "Delete") { + if (state.controls) { + const target = (state.controls as any).getTarget(new THREE.Vector3()); + (state.controls as any).setTarget(target.x, 0, target.z, true); + } + setselectedFloorItem(null); + { + setObjectPosition({ x: undefined, y: undefined, z: undefined }); + setObjectScale({ x: undefined, y: undefined, z: undefined }); + setObjectRotation({ x: undefined, y: undefined, z: undefined }); + } + } + }, [activeTool]); + + return ( + <> + {(selectedFloorItem && transformMode) && + + } + + ); +} diff --git a/app/src/modules/scene/environment/ground.tsx b/app/src/modules/scene/environment/ground.tsx new file mode 100644 index 0000000..0251ba6 --- /dev/null +++ b/app/src/modules/scene/environment/ground.tsx @@ -0,0 +1,22 @@ +import * as THREE from 'three'; +import { useToggleView } from '../../../store/store'; +import * as CONSTANTS from '../../../types/world/worldConstants'; + +const Ground = ({ grid, plane }: any) => { + const { toggleView, setToggleView } = useToggleView(); + + return ( + + + + + + + + + + + ) +} + +export default Ground; \ No newline at end of file diff --git a/app/src/modules/scene/environment/shadow.tsx b/app/src/modules/scene/environment/shadow.tsx new file mode 100644 index 0000000..7db2104 --- /dev/null +++ b/app/src/modules/scene/environment/shadow.tsx @@ -0,0 +1,84 @@ +import { useRef, useEffect} from 'react'; +import { useThree } from '@react-three/fiber'; +import * as THREE from 'three'; +import { useAzimuth, useElevation, useShadows, useSunPosition, useFloorItems, useWallItems } from '../../../store/store'; +import * as CONSTANTS from '../../../types/world/worldConstants'; +const shadowWorker = new Worker(new URL('../../../services/factoryBuilder/webWorkers/shadowWorker', import.meta.url)); + +export default function Shadows() { + const { shadows, setShadows } = useShadows(); + const { sunPosition, setSunPosition } = useSunPosition(); + const lightRef = useRef(null); + const targetRef = useRef(null); + const { controls, gl } = useThree(); + const { elevation, setElevation } = useElevation(); + const { azimuth, setAzimuth } = useAzimuth(); + const { floorItems } = useFloorItems(); + const { wallItems } = useWallItems(); + + useEffect(() => { + gl.shadowMap.enabled = true; + gl.shadowMap.type = THREE.PCFShadowMap; + }, [gl, floorItems, wallItems]); + + useEffect(() => { + if (lightRef.current && targetRef.current) { + lightRef.current.target = targetRef.current; + } + }, []); + + useEffect(() => { + shadowWorker.onmessage = (event) => { + const { lightPosition, controlsTarget } = event.data; + if (lightRef.current && targetRef.current && controls) { + lightRef.current.position.copy(lightPosition); + targetRef.current.position.copy(controlsTarget); + } + }; + }, [shadowWorker, controls]); + + const updateShadows = () => { + if (controls && shadowWorker) { + const offsetDistance = CONSTANTS.shadowConfig.shadowOffset; + const controlsTarget = (controls as any).getTarget(); + shadowWorker.postMessage({ controlsTarget, sunPosition, offsetDistance }); + } + }; + + useEffect(() => { + if (controls && shadows) { + updateShadows(); + (controls as any).addEventListener('update', updateShadows); + return () => { + (controls as any).removeEventListener('update', updateShadows); + }; + } + }, [controls, elevation, azimuth, shadows]); + + return ( + <> + {/* {(lightRef.current?.shadow) && + + } */} + + + + + + + + ); +} \ No newline at end of file diff --git a/app/src/modules/scene/environment/sky.tsx b/app/src/modules/scene/environment/sky.tsx new file mode 100644 index 0000000..b2bb210 --- /dev/null +++ b/app/src/modules/scene/environment/sky.tsx @@ -0,0 +1,49 @@ +import * as THREE from 'three'; +import { Sky } from "@react-three/drei"; +import { useAzimuth, useElevation, useSunPosition } from "../../../store/store"; +import { useEffect, useRef, useState } from "react"; +import * as CONSTANTS from '../../../types/world/worldConstants'; + +export default function Sun() { + const { elevation, setElevation } = useElevation(); + const { sunPosition, setSunPosition } = useSunPosition(); + const { azimuth, setAzimuth } = useAzimuth(); + const [turbidity, setTurbidity] = useState(CONSTANTS.skyConfig.defaultTurbidity); + const sunPositionRef = useRef(new THREE.Vector3(0, 0, 0)); + const [_, forceUpdate] = useState(0); + const maxTurbidity = CONSTANTS.skyConfig.maxTurbidity; + const minTurbidity = CONSTANTS.skyConfig.minTurbidity; + + useEffect(() => { + const phi = THREE.MathUtils.degToRad(90 - elevation); + const theta = THREE.MathUtils.degToRad(azimuth); + + const computedTurbidity = minTurbidity + ((elevation - 2) / (90 - 2)) * (maxTurbidity - minTurbidity); + setTurbidity(computedTurbidity); + + sunPositionRef.current.setFromSphericalCoords(1, phi, theta); + setSunPosition(sunPositionRef.current); + forceUpdate(prev => prev + 1); + }, [elevation, azimuth]); + + return ( + <> + {(azimuth !== undefined && elevation !== undefined) && ( + <> + + + )} + + ); +} diff --git a/app/src/modules/scene/mqttTemp/drieHtmlTemp.tsx b/app/src/modules/scene/mqttTemp/drieHtmlTemp.tsx new file mode 100644 index 0000000..8604bcc --- /dev/null +++ b/app/src/modules/scene/mqttTemp/drieHtmlTemp.tsx @@ -0,0 +1,109 @@ +import { Html } from "@react-three/drei"; +import * as THREE from "three"; +import * as Types from "../../../types/world/worldTypes"; +import { useDrieTemp, useDrieUIValue } from "../../../store/store" +import UI from "./ui"; +import { useEffect } from "react"; +import { useThree } from "@react-three/fiber"; + +export default function DrieHtmlTemp({ itemsGroup }: { itemsGroup: Types.RefGroup }) { + const { drieTemp, setDrieTemp } = useDrieTemp(); + const { drieUIValue, setDrieUIValue } = useDrieUIValue(); + const state = useThree(); + const { camera, raycaster } = state; + + useEffect(() => { + const canvasElement = state.gl.domElement; + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = true; + drag = false; + } + }; + + const onMouseMove = () => { + if (isLeftMouseDown) { + drag = true; + } + }; + + const onMouseUp = (evt: any) => { + if (evt.button === 0) { + isLeftMouseDown = false; + if (drag) return; + if (!itemsGroup.current) return + let intersects = raycaster.intersectObjects(itemsGroup.current.children, true); + if (intersects.length > 0) { + let currentObject = intersects[0].object; + + while (currentObject) { + if (currentObject.name === "Scene") { + break; + } + currentObject = currentObject.parent as THREE.Object3D; + } + if (currentObject && (currentObject.userData.name === "SV2 Controll pannel" || currentObject.userData.name === "forklift")) { + const worldPos = new THREE.Vector3(); + currentObject.getWorldPosition(worldPos); + + const rightOffset = new THREE.Vector3(1, 0, 0); + const upOffset = new THREE.Vector3(0, 1, 0); + + currentObject.localToWorld(rightOffset); + currentObject.localToWorld(upOffset); + + const finalPosition = worldPos.clone().addScaledVector(rightOffset.sub(currentObject.position).normalize(), 2.5).addScaledVector(upOffset.sub(currentObject.position).normalize(), 2.3); + + setDrieTemp(finalPosition); + } else { + setDrieTemp(undefined); + } + } + else { + setDrieTemp(undefined); + } + } + }; + + + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + }; + }, []) + + return ( + <> + {drieTemp && + + + + + + } + + ) +} \ No newline at end of file diff --git a/app/src/modules/scene/mqttTemp/ui.jsx b/app/src/modules/scene/mqttTemp/ui.jsx new file mode 100644 index 0000000..cafff38 --- /dev/null +++ b/app/src/modules/scene/mqttTemp/ui.jsx @@ -0,0 +1,141 @@ +export default function UI({ temperature, humidity, touch, header }) { + return ( +
+
+ {header ? header : "Sensor Details"} +
+
+
+
+ + + + + +
+
+ Temperature +
+
+ {temperature} +
+
+
+
+ + + + + +
+
+ Humidity +
+
+ {humidity} +
+
+
+
+
+
+ Touch Sensor +
+
+ {touch === "True" ? "Active" : "In active"} +
+
+
+
+ ); +} diff --git a/app/src/modules/scene/postProcessing/postProcessing.tsx b/app/src/modules/scene/postProcessing/postProcessing.tsx new file mode 100644 index 0000000..1c6c7ab --- /dev/null +++ b/app/src/modules/scene/postProcessing/postProcessing.tsx @@ -0,0 +1,111 @@ +import * as THREE from 'three' +import { EffectComposer, N8AO, Outline } from '@react-three/postprocessing' +import { BlendFunction } from 'postprocessing' +import { useDeletableFloorItem, useSelectedEventSphere, useSelectedPath, useSelectedWallItem, useselectedFloorItem } from '../../../store/store'; +import * as Types from '../../../types/world/worldTypes' +import * as CONSTANTS from '../../../types/world/worldConstants'; + +export default function PostProcessing() { + const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem(); + const { selectedWallItem, setSelectedWallItem } = useSelectedWallItem(); + const { selectedFloorItem, setselectedFloorItem } = useselectedFloorItem(); + const { selectedEventSphere } = useSelectedEventSphere(); + const { selectedPath } = useSelectedPath(); + + function flattenChildren(children: any[]) { + const allChildren: any[] = []; + children.forEach(child => { + allChildren.push(child); + if (child.children && child.children.length > 0) { + allChildren.push(...flattenChildren(child.children)); + } + }); + return allChildren; + } + + return ( + <> + + + {deletableFloorItem && + + } + {selectedWallItem && + child.name !== "CSG_REF" + ) + } + selectionLayer={10} + width={3000} + blendFunction={BlendFunction.ALPHA} + edgeStrength={5} + resolutionScale={2} + pulseSpeed={0} + visibleEdgeColor={CONSTANTS.outlineConfig.assetSelectColor} + hiddenEdgeColor={CONSTANTS.outlineConfig.assetSelectColor} + blur={true} + xRay={true} + />} + {selectedFloorItem && + + } + {selectedEventSphere && + + } + {selectedPath && + + } + + + ) +} \ No newline at end of file diff --git a/app/src/modules/scene/scene.tsx b/app/src/modules/scene/scene.tsx new file mode 100644 index 0000000..d3e37fd --- /dev/null +++ b/app/src/modules/scene/scene.tsx @@ -0,0 +1,56 @@ +import { useMemo } from "react"; +import { Canvas } from "@react-three/fiber"; +import { Environment, KeyboardControls } from "@react-three/drei"; + +import World from "./world/world"; +import Controls from "./controls/controls"; +import TransformControl from "./controls/transformControls"; +import PostProcessing from "./postProcessing/postProcessing" +import Sun from "./environment/sky"; +import CamModelsGroup from "../collaboration/collabCams"; +import Shadows from "./environment/shadow"; +import MqttEvents from "../../services/factoryBuilder/mqtt/mqttEvents"; + +import background from "../../assets/textures/hdr/mudroadpuresky2k.hdr"; +import SelectionControls from "./controls/selection/selectionControls"; +import MeasurementTool from "./tools/measurementTool"; +import Simulation from "../simulation/simulation"; +// import Simulation from "./simulationtemp/simulation"; + +export default function Scene() { + + const map = useMemo(() => [ + { name: "forward", keys: ["ArrowUp", "w", "W"] }, + { name: "backward", keys: ["ArrowDown", "s", "S"] }, + { name: "left", keys: ["ArrowLeft", "a", "A"] }, + { name: "right", keys: ["ArrowRight", "d", "D"] }, + // { name: "jump", keys: ["Space"] }, + ], []) + + return ( + + { + e.preventDefault(); + }} + > + + + + + + {/* */} + + + + + + + + + + ); +} diff --git a/app/src/modules/scene/tools/measurementTool.tsx b/app/src/modules/scene/tools/measurementTool.tsx new file mode 100644 index 0000000..3e2a5a7 --- /dev/null +++ b/app/src/modules/scene/tools/measurementTool.tsx @@ -0,0 +1,190 @@ +import * as THREE from 'three'; +import { useEffect, useRef, useState } from 'react'; +import { useThree, useFrame } from '@react-three/fiber'; +import { useToolMode } from '../../../store/store'; +import { Html } from '@react-three/drei'; + +const MeasurementTool = () => { + const { gl, raycaster, pointer, camera, scene } = useThree(); + const { toolMode } = useToolMode(); + + const [points, setPoints] = useState([]); + const [tubeGeometry, setTubeGeometry] = useState(null); + const groupRef = useRef(null); + const [startConePosition, setStartConePosition] = useState(null); + const [endConePosition, setEndConePosition] = useState(null); + const [startConeQuaternion, setStartConeQuaternion] = useState(new THREE.Quaternion()); + const [endConeQuaternion, setEndConeQuaternion] = useState(new THREE.Quaternion()); + const [coneSize, setConeSize] = useState({ radius: 0.2, height: 0.5 }); + + + const MIN_RADIUS = 0.001, MAX_RADIUS = 0.1; + const MIN_CONE_RADIUS = 0.01, MAX_CONE_RADIUS = 0.4; + const MIN_CONE_HEIGHT = 0.035, MAX_CONE_HEIGHT = 2.0; + + useEffect(() => { + const canvasElement = gl.domElement; + let drag = false; + let isLeftMouseDown = false; + + const onMouseDown = () => { + isLeftMouseDown = true; + drag = false; + }; + + const onMouseUp = (evt: any) => { + isLeftMouseDown = false; + if (evt.button === 0 && !drag) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(scene.children, true).filter(intersect => !intersect.object.name.includes("Roof") && !intersect.object.name.includes("MeasurementReference") && !(intersect.object.type === "GridHelper")); + + if (intersects.length > 0) { + const intersectionPoint = intersects[0].point.clone(); + if (points.length < 2) { + setPoints([...points, intersectionPoint]); + } else { + setPoints([intersectionPoint]); + } + } + } + }; + + const onMouseMove = () => { + if (isLeftMouseDown) drag = true; + }; + + const onContextMenu = (evt: any) => { + evt.preventDefault(); + if (!drag) { + evt.preventDefault(); + setPoints([]); + setTubeGeometry(null); + } + }; + + if (toolMode === "MeasurementScale") { + canvasElement.addEventListener("pointerdown", onMouseDown); + canvasElement.addEventListener("pointermove", onMouseMove); + canvasElement.addEventListener("pointerup", onMouseUp); + canvasElement.addEventListener("contextmenu", onContextMenu); + } else { + resetMeasurement(); + setPoints([]); + } + + return () => { + canvasElement.removeEventListener("pointerdown", onMouseDown); + canvasElement.removeEventListener("pointermove", onMouseMove); + canvasElement.removeEventListener("pointerup", onMouseUp); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [toolMode, camera, raycaster, pointer, scene, points]); + + useFrame(() => { + if (points.length === 1) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(scene.children, true).filter(intersect => !intersect.object.name.includes("Roof") && !intersect.object.name.includes("MeasurementReference") && !(intersect.object.type === "GridHelper")); + + if (intersects.length > 0) { + updateMeasurement(points[0], intersects[0].point); + } + } else if (points.length === 2) { + updateMeasurement(points[0], points[1]); + } else { + resetMeasurement(); + } + }); + + const updateMeasurement = (start: THREE.Vector3, end: THREE.Vector3) => { + const distance = start.distanceTo(end); + + const radius = THREE.MathUtils.clamp(distance * 0.02, MIN_RADIUS, MAX_RADIUS); + const coneRadius = THREE.MathUtils.clamp(distance * 0.05, MIN_CONE_RADIUS, MAX_CONE_RADIUS); + const coneHeight = THREE.MathUtils.clamp(distance * 0.2, MIN_CONE_HEIGHT, MAX_CONE_HEIGHT); + + setConeSize({ radius: coneRadius, height: coneHeight }); + + const direction = new THREE.Vector3().subVectors(end, start).normalize(); + + const offset = direction.clone().multiplyScalar(coneHeight * 0.5); + + let tubeStart = start.clone().add(offset); + let tubeEnd = end.clone().sub(offset); + + tubeStart.y = Math.max(tubeStart.y, 0); + tubeEnd.y = Math.max(tubeEnd.y, 0); + + const curve = new THREE.CatmullRomCurve3([tubeStart, tubeEnd]); + setTubeGeometry(new THREE.TubeGeometry(curve, 20, radius, 8, false)); + + setStartConePosition(tubeStart); + setEndConePosition(tubeEnd); + setStartConeQuaternion(getArrowOrientation(start, end)); + setEndConeQuaternion(getArrowOrientation(end, start)); + }; + + const resetMeasurement = () => { + setTubeGeometry(null); + setStartConePosition(null); + setEndConePosition(null); + }; + + const getArrowOrientation = (start: THREE.Vector3, end: THREE.Vector3) => { + const direction = new THREE.Vector3().subVectors(end, start).normalize().negate(); + const quaternion = new THREE.Quaternion(); + quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); + return quaternion; + }; + + useEffect(() => { + if (points.length === 2) { + console.log(points[0].distanceTo(points[1])); + } + }, [points]) + + return ( + + {startConePosition && ( + + + + + )} + {endConePosition && ( + + + + + )} + {tubeGeometry && ( + + + + )} + + {startConePosition && endConePosition && ( + +
{startConePosition.distanceTo(endConePosition).toFixed(2)} m
+ + )} +
+ ); + +}; + +export default MeasurementTool; diff --git a/app/src/modules/scene/world/world.tsx b/app/src/modules/scene/world/world.tsx new file mode 100644 index 0000000..b83fa1c --- /dev/null +++ b/app/src/modules/scene/world/world.tsx @@ -0,0 +1,336 @@ +////////// Three and React Three Fiber Imports ////////// + +import * as THREE from "three"; +import { useEffect, useRef, useState } from "react"; +import { useThree, useFrame } from "@react-three/fiber"; + +////////// Component Imports ////////// + +import DistanceText from "../../builder/geomentries/lines/distanceText"; +import ReferenceDistanceText from "../../builder/geomentries/lines/referenceDistanceText"; + +////////// Assests Imports ////////// + +import arch from "../../../assets/gltf-glb/arch.glb"; +import door from "../../../assets/gltf-glb/door.glb"; +import Window from "../../../assets/gltf-glb/window.glb"; + +////////// Zustand State Imports ////////// + +import { + useToggleView, + useDeletePointOrLine, + useMovePoint, + useActiveLayer, + useSocketStore, + useWallVisibility, + useRoofVisibility, + useShadows, + useUpdateScene, + useWalls, + useToolMode +} from "../../../store/store"; + +////////// 3D Function Imports ////////// + +import loadWalls from "../../builder/geomentries/walls/loadWalls"; + +import * as Types from "../../../types/world/worldTypes"; + +import SocketResponses from "../../collaboration/socketResponses.dev"; +import FloorItemsGroup from "../../builder/groups/floorItemsGroup"; +import FloorPlanGroup from "../../builder/groups/floorPlanGroup"; +import FloorGroup from "../../builder/groups/floorGroup"; +import FloorGroupAilse from "../../builder/groups/floorGroupAisle"; +import Draw from "../../builder/functions/draw"; +import WallsAndWallItems from "../../builder/groups/wallsAndWallItems"; +import Ground from "../environment/ground"; +// import ZoneGroup from "../groups/zoneGroup1"; +import { findEnvironment } from "../../../services/factoryBuilder/environment/findEnvironment"; +import Layer2DVisibility from "../../builder/geomentries/layers/layer2DVisibility"; +import DrieHtmlTemp from "../mqttTemp/drieHtmlTemp"; +import ZoneGroup from "../../builder/groups/zoneGroup"; + +export default function World() { + 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. + + 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 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 { updateScene, setUpdateScene } = useUpdateScene(); + const { walls, setWalls } = useWalls(); + const [RefTextupdate, setRefTextUpdate] = useState(-1000); + + + // 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); + + ////////// 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", + }, + }; + + ////////// All Toggle's ////////// + + useEffect(() => { + setRefTextUpdate((prevUpdate) => 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(() => { + 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); + } + } + fetchVisibility(); + }, []) + + ////////// 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) + } + }); + + ////////// Return ////////// + + return ( + <> + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + ); +} \ No newline at end of file diff --git a/app/src/modules/simulation/behaviour/behaviour.tsx b/app/src/modules/simulation/behaviour/behaviour.tsx new file mode 100644 index 0000000..f0289f9 --- /dev/null +++ b/app/src/modules/simulation/behaviour/behaviour.tsx @@ -0,0 +1,77 @@ +import { useFloorItems } from '../../../store/store'; +import * as THREE from 'three'; +import * as Types from '../../../types/world/worldTypes'; +import { useEffect } from 'react'; + + +interface Path { + modeluuid: string; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + events: { uuid: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | []; + triggers: { uuid: string; type: string; isUsed: boolean }[] | []; + }[]; + pathPosition: [number, number, number]; + pathRotation: [number, number, number]; + speed: number; +} + +function Behaviour({ setSimulationPaths }: { setSimulationPaths: any }) { + const { floorItems } = useFloorItems(); + + useEffect(() => { + const newPaths: Path[] = []; + + floorItems.forEach((item: Types.FloorItemType) => { + if (item.modelfileID === "6633215057b31fe671145959") { + const point1Position = new THREE.Vector3(0, 1.25, 3.3); + const middlePointPosition = new THREE.Vector3(0, 1.25, 0); + const point2Position = new THREE.Vector3(0, 1.25, -3.3); + + const point1UUID = THREE.MathUtils.generateUUID(); + const point2UUID = THREE.MathUtils.generateUUID(); + const middlePointUUID = THREE.MathUtils.generateUUID(); + + const newPath: Path = { + modeluuid: item.modeluuid, + points: [ + { + uuid: point1UUID, + position: [point1Position.x, point1Position.y, point1Position.z], + rotation: [0, 0, 0], + events: [{ uuid: THREE.MathUtils.generateUUID(), type: 'Inherit', material: 'Inherit', delay: 'Inherit', spawnInterval: 'Inherit', isUsed: false }], + triggers: [], + }, + { + uuid: middlePointUUID, + position: [middlePointPosition.x, middlePointPosition.y, middlePointPosition.z], + rotation: [0, 0, 0], + events: [{ uuid: THREE.MathUtils.generateUUID(), type: 'Inherit', material: 'Inherit', delay: 'Inherit', spawnInterval: 'Inherit', isUsed: false }], + triggers: [], + }, + { + uuid: point2UUID, + position: [point2Position.x, point2Position.y, point2Position.z], + rotation: [0, 0, 0], + events: [{ uuid: THREE.MathUtils.generateUUID(), type: 'Inherit', material: 'Inherit', delay: 'Inherit', spawnInterval: 'Inherit', isUsed: false }], + triggers: [], + }, + ], + pathPosition: [...item.position], + pathRotation: [item.rotation.x, item.rotation.y, item.rotation.z], + speed: 1, + }; + + newPaths.push(newPath); + } + }); + + setSimulationPaths(newPaths); + }, [floorItems]); + + return null; +} + +export default Behaviour; diff --git a/app/src/modules/simulation/events.tsx/eventsControl.tsx b/app/src/modules/simulation/events.tsx/eventsControl.tsx new file mode 100644 index 0000000..4f273a2 --- /dev/null +++ b/app/src/modules/simulation/events.tsx/eventsControl.tsx @@ -0,0 +1,55 @@ +import { useControls } from 'leva'; +import { useSelectedEventSphere } from '../../../store/store'; + +interface Path { + modeluuid: string; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + events: string; + triggers: string; + }[]; + position: [number, number, number]; + rotation: [number, number, number]; +} + +const EventsControl = ({ simulationPaths, setSimulationPaths }: { simulationPaths: Path[], setSimulationPaths: any }) => { + const { selectedEventSphere, setSelectedEventSphere } = useSelectedEventSphere(); + + const { events, triggers }: any = useControls({ + events: { + value: selectedEventSphere?.point?.userData?.events || '', + options: ['Event1', 'Event2', 'Event3'], + onChange: (newEvent: string) => updatePathData(newEvent, 'events') + }, + triggers: { + value: selectedEventSphere?.point?.userData?.triggers || '', + options: ['None', 'Trigger1', 'Trigger2', 'Trigger3'], + onChange: (newTrigger: string) => updatePathData(newTrigger, 'triggers') + }, + }); + + function updatePathData(value: string, key: 'events' | 'triggers') { + if (!selectedEventSphere) return; + + const updatedPaths = simulationPaths.map((path) => + path.modeluuid === selectedEventSphere.path.modeluuid + ? { + ...path, + points: path.points.map((point) => + point.uuid === selectedEventSphere.point.uuid + ? { ...point, [key]: value } + : point + ), + } + : path + ); + console.log('updatedPaths: ', updatedPaths); + setSimulationPaths(updatedPaths); + } + + return null; +}; + +export default EventsControl; diff --git a/app/src/modules/simulation/path/pathConnector.tsx b/app/src/modules/simulation/path/pathConnector.tsx new file mode 100644 index 0000000..c8ab607 --- /dev/null +++ b/app/src/modules/simulation/path/pathConnector.tsx @@ -0,0 +1,287 @@ +import { useFrame, useThree } from '@react-three/fiber'; +import React, { useEffect, useState } from 'react'; +import * as THREE from 'three'; +import { QuadraticBezierLine } from '@react-three/drei'; +import { useConnections, useIsConnecting, useSimulationPaths } from '../../../store/store'; +import useModuleStore from '../../../store/useModuleStore'; + +function PathConnector({ pathsGroupRef }: { pathsGroupRef: React.MutableRefObject }) { + const { activeModule } = useModuleStore(); + const { gl, raycaster, scene, pointer, camera } = useThree(); + const { connections, setConnections, addConnection } = useConnections(); + const { isConnecting, setIsConnecting } = useIsConnecting(); + const { simulationPaths, setSimulationPaths } = useSimulationPaths(); + + const [firstSelected, setFirstSelected] = useState<{ pathUUID: string; sphereUUID: string; position: THREE.Vector3; isCorner: boolean; } | null>(null); + const [currentLine, setCurrentLine] = useState<{ start: THREE.Vector3, end: THREE.Vector3, mid: THREE.Vector3 } | null>(null); + const [hoveredSphere, setHoveredSphere] = useState<{ sphereUUID: string, position: THREE.Vector3 } | null>(null); + const [helperlineColor, setHelperLineColor] = useState('red'); + + useEffect(() => { + const canvasElement = gl.domElement; + let drag = false; + let MouseDown = false; + + const onMouseDown = () => { + MouseDown = true; + drag = false; + }; + + const onMouseUp = () => { + MouseDown = false; + }; + + const onMouseMove = () => { + if (MouseDown) { + drag = true; + } + }; + + const onContextMenu = (evt: MouseEvent) => { + evt.preventDefault(); + if (drag || evt.button === 0) return; + + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(pathsGroupRef.current.children, true); + + if (intersects.length > 0) { + const intersected = intersects[0].object; + + if (intersected.name.includes("event-sphere")) { + const pathUUID = intersected.userData.path.modeluuid; + const sphereUUID = intersected.uuid; + const worldPosition = new THREE.Vector3(); + intersected.getWorldPosition(worldPosition); + + const isStartOrEnd = intersected.userData.path.points.length > 0 && ( + sphereUUID === intersected.userData.path.points[0].uuid || + sphereUUID === intersected.userData.path.points[intersected.userData.path.points.length - 1].uuid + ); + + if (pathUUID) { + const isAlreadyConnected = connections.some((connection) => + connection.fromUUID === sphereUUID || + connection.toConnections.some(conn => conn.toUUID === sphereUUID) + ); + + if (isAlreadyConnected) { + console.log("Sphere is already connected. Ignoring."); + return; + } + + if (!firstSelected) { + setFirstSelected({ + pathUUID, + sphereUUID, + position: worldPosition, + isCorner: isStartOrEnd + }); + setIsConnecting(true); + } else { + if (firstSelected.sphereUUID === sphereUUID) return; + + if (firstSelected.pathUUID === pathUUID) { + console.log("Cannot connect spheres on the same path."); + return; + } + if (!firstSelected.isCorner && !isStartOrEnd) { + console.log("At least one of the selected spheres must be a start or end point."); + return; + } + + addConnection({ + fromPathUUID: firstSelected.pathUUID, + fromUUID: firstSelected.sphereUUID, + toConnections: [{ toPathUUID: pathUUID, toUUID: sphereUUID }] + }); + + setFirstSelected(null); + setCurrentLine(null); + setIsConnecting(false); + setHoveredSphere(null); + } + } + } + } else { + setFirstSelected(null); + setCurrentLine(null); + setIsConnecting(false); + setHoveredSphere(null); + } + }; + + if (activeModule === 'simulation') { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("contextmenu", onContextMenu); + } else { + setFirstSelected(null); + setCurrentLine(null); + setIsConnecting(false); + setHoveredSphere(null); + } + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [camera, scene, raycaster, firstSelected, connections]); + + useFrame(() => { + if (firstSelected) { + raycaster.setFromCamera(pointer, camera); + const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.userData.isPathObject && + !(intersect.object.type === "GridHelper") + ); + + let point: THREE.Vector3 | null = null; + let snappedSphere: { sphereUUID: string, position: THREE.Vector3, pathUUID: string, isCorner: boolean } | null = null; + let isInvalidConnection = false; + + if (intersects.length > 0) { + point = intersects[0].point; + + if (point.y < 0.05) { + point = new THREE.Vector3(point.x, 0.05, point.z); + } + } + + const sphereIntersects = raycaster.intersectObjects(pathsGroupRef.current.children, true).filter((obj) => + obj.object.name.includes("event-sphere") + ); + + if (sphereIntersects.length > 0) { + const sphere = sphereIntersects[0].object; + const sphereUUID = sphere.uuid; + const spherePosition = new THREE.Vector3(); + sphere.getWorldPosition(spherePosition); + const pathUUID = sphere.userData.path.modeluuid; + + const isStartOrEnd = sphere.userData.path.points.length > 0 && ( + sphereUUID === sphere.userData.path.points[0].uuid || + sphereUUID === sphere.userData.path.points[sphere.userData.path.points.length - 1].uuid + ); + + const isAlreadyConnected = connections.some((connection) => + connection.fromUUID === sphereUUID || + connection.toConnections.some(conn => conn.toUUID === sphereUUID) + ); + + if ( + !isAlreadyConnected && + firstSelected.sphereUUID !== sphereUUID && + firstSelected.pathUUID !== pathUUID && + (firstSelected.isCorner || isStartOrEnd) + ) { + snappedSphere = { sphereUUID, position: spherePosition, pathUUID, isCorner: isStartOrEnd }; + } else { + isInvalidConnection = true; + } + } + + if (snappedSphere) { + setHoveredSphere(snappedSphere); + point = snappedSphere.position; + } else { + setHoveredSphere(null); + } + + if (point) { + const distance = firstSelected.position.distanceTo(point); + const heightFactor = Math.max(0.5, distance * 0.2); + const midPoint = new THREE.Vector3( + (firstSelected.position.x + point.x) / 2, + Math.max(firstSelected.position.y, point.y) + heightFactor, + (firstSelected.position.z + point.z) / 2 + ); + + setCurrentLine({ + start: firstSelected.position, + end: point, + mid: midPoint, + }); + + setIsConnecting(true); + + if (sphereIntersects.length > 0) { + setHelperLineColor(isInvalidConnection ? 'red' : '#6cf542'); + } else { + setHelperLineColor('yellow'); + } + } else { + setCurrentLine(null); + setIsConnecting(false); + } + } else { + setCurrentLine(null); + setIsConnecting(false); + } + }); + + + useEffect(() => { + console.log('connections: ', connections); + }, [connections]); + + return ( + <> + {connections.map((connection, index) => { + const fromSphere = scene.getObjectByProperty('uuid', connection.fromUUID); + const toSphere = scene.getObjectByProperty('uuid', connection.toConnections[0].toUUID); + + if (fromSphere && toSphere) { + const fromWorldPosition = new THREE.Vector3(); + const toWorldPosition = new THREE.Vector3(); + fromSphere.getWorldPosition(fromWorldPosition); + toSphere.getWorldPosition(toWorldPosition); + + const distance = fromWorldPosition.distanceTo(toWorldPosition); + const heightFactor = Math.max(0.5, distance * 0.2); + + const midPoint = new THREE.Vector3( + (fromWorldPosition.x + toWorldPosition.x) / 2, + Math.max(fromWorldPosition.y, toWorldPosition.y) + heightFactor, + (fromWorldPosition.z + toWorldPosition.z) / 2 + ); + + return ( + + ); + } + return null; + })} + + {currentLine && ( + + )} + + ); +} + +export default PathConnector; \ No newline at end of file diff --git a/app/src/modules/simulation/path/pathCreation.tsx b/app/src/modules/simulation/path/pathCreation.tsx new file mode 100644 index 0000000..f79d646 --- /dev/null +++ b/app/src/modules/simulation/path/pathCreation.tsx @@ -0,0 +1,163 @@ +import * as THREE from 'three'; +import { useRef, useState, useEffect } from 'react'; +import { Sphere, TransformControls } from '@react-three/drei'; +import { useIsConnecting, useRenderDistance, useSelectedEventSphere, useSelectedPath, useSimulationPaths } from '../../../store/store'; +import { useFrame, useThree } from '@react-three/fiber'; + +interface Path { + modeluuid: string; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + events: { uuid: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | []; + triggers: { uuid: string; type: string; isUsed: boolean }[] | []; + }[]; + pathPosition: [number, number, number]; + pathRotation: [number, number, number]; + speed: number; +} + +function PathCreation({ pathsGroupRef }: { pathsGroupRef: React.MutableRefObject }) { + const { renderDistance } = useRenderDistance(); + const { setSelectedEventSphere, selectedEventSphere } = useSelectedEventSphere(); + const { setSelectedPath } = useSelectedPath(); + const { simulationPaths, setSimulationPaths } = useSimulationPaths(); + const { isConnecting, setIsConnecting } = useIsConnecting(); + const { camera } = useThree(); + + const groupRefs = useRef<{ [key: string]: THREE.Group }>({}); + const sphereRefs = useRef<{ [key: string]: THREE.Mesh }>({}); + const transformRef = useRef(null); + const [transformMode, setTransformMode] = useState<'translate' | 'rotate' | null>(null); + + useEffect(() => { + setTransformMode(null); + const handleKeyDown = (e: KeyboardEvent) => { + if (!selectedEventSphere) return; + if (e.key === 'g') { + setTransformMode(prev => prev === 'translate' ? null : 'translate'); + } + if (e.key === 'r') { + setTransformMode(prev => prev === 'rotate' ? null : 'rotate'); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedEventSphere]); + + useFrame(() => { + Object.values(groupRefs.current).forEach(group => { + if (group) { + const distance = new THREE.Vector3(...group.position.toArray()).distanceTo(camera.position); + group.visible = distance <= renderDistance; + } + }); + }); + + const updateSimulationPaths = () => { + if (!selectedEventSphere) return; + + const updatedPaths: Path[] = simulationPaths.map((path) => ({ + ...path, + points: path.points.map((point) => + point.uuid === selectedEventSphere.point.uuid + ? { + ...point, + position: [ + selectedEventSphere.point.position.x, + selectedEventSphere.point.position.y, + selectedEventSphere.point.position.z, + ], + rotation: [ + selectedEventSphere.point.rotation.x, + selectedEventSphere.point.rotation.y, + selectedEventSphere.point.rotation.z, + ] + } + : point + ), + })); + + setSimulationPaths(updatedPaths); + }; + + + return ( + + {simulationPaths.map((path) => { + const points = path.points.map(point => new THREE.Vector3(...point.position)); + + return ( + (groupRefs.current[path.modeluuid] = el!)} + position={path.pathPosition} + rotation={path.pathRotation} + onClick={(e) => { + if (isConnecting) return; + e.stopPropagation(); + setSelectedPath({ path, group: groupRefs.current[path.modeluuid] }); + setSelectedEventSphere(null); + setTransformMode(null); + }} + onPointerMissed={() => { + setSelectedPath(null); + }} + > + {path.points.map((point, index) => ( + (sphereRefs.current[point.uuid] = el!)} + onClick={(e) => { + if (isConnecting) return; + e.stopPropagation(); + setSelectedEventSphere({ + path, + point: sphereRefs.current[point.uuid] + }); + setSelectedPath(null); + }} + userData={{ point, path }} + onPointerMissed={() => setSelectedEventSphere(null)} + > + + + ))} + + {points.slice(0, -1).map((point, index) => { + const nextPoint = points[index + 1]; + const segmentCurve = new THREE.CatmullRomCurve3([point, nextPoint]); + const tubeGeometry = new THREE.TubeGeometry(segmentCurve, 20, 0.1, 16, false); + + return ( + + + + ); + })} + + ); + })} + + {selectedEventSphere && transformMode && ( + + )} + + ); +} + +export default PathCreation; diff --git a/app/src/modules/simulation/simulation.tsx b/app/src/modules/simulation/simulation.tsx new file mode 100644 index 0000000..6851e9d --- /dev/null +++ b/app/src/modules/simulation/simulation.tsx @@ -0,0 +1,46 @@ +import { useState, useEffect, useRef } from 'react'; +import { useConnections, useFloorItems, useSelectedEventSphere, useSelectedPath, useSimulationPaths } from '../../store/store'; +import { useThree } from '@react-three/fiber'; +import * as THREE from 'three'; +import Behaviour from './behaviour/behaviour'; +import PathCreation from './path/pathCreation'; +import PathConnector from './path/pathConnector'; +import useModuleStore from '../../store/useModuleStore'; + +function Simulation() { + const { activeModule } = useModuleStore(); + const pathsGroupRef = useRef() as React.MutableRefObject; + const { simulationPaths, setSimulationPaths } = useSimulationPaths(); + const { connections, setConnections, addConnection, removeConnection } = useConnections(); + const [processes, setProcesses] = useState([]); + + useEffect(() => { + console.log('simulationPaths: ', simulationPaths); + }, [simulationPaths]); + + // useEffect(() => { + // if (selectedEventSphere) { + // console.log('selectedEventSphere: ', selectedEventSphere); + // } + // }, [selectedEventSphere]); + + // useEffect(() => { + // if (selectedPath) { + // console.log('selectedPath: ', selectedPath); + // } + // }, [selectedPath]); + + return ( + <> + {activeModule === 'simulation' && ( + <> + + + + + )} + + ); +} + +export default Simulation; \ No newline at end of file diff --git a/app/src/modules/simulation/simulationtemp/collider/colliderCreator.tsx b/app/src/modules/simulation/simulationtemp/collider/colliderCreator.tsx new file mode 100644 index 0000000..3f3a487 --- /dev/null +++ b/app/src/modules/simulation/simulationtemp/collider/colliderCreator.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function ColliderCreator() { + return ( + <> + ) +} + +export default ColliderCreator \ No newline at end of file diff --git a/app/src/modules/simulation/simulationtemp/path/pathCreator.tsx b/app/src/modules/simulation/simulationtemp/path/pathCreator.tsx new file mode 100644 index 0000000..f77894b --- /dev/null +++ b/app/src/modules/simulation/simulationtemp/path/pathCreator.tsx @@ -0,0 +1,404 @@ +import { useEffect, useState } from 'react'; +import * as THREE from 'three'; +import { useThree, useFrame } from '@react-three/fiber'; +import { Line, TransformControls } from '@react-three/drei'; +import { useDrawMaterialPath } from '../../../../store/store'; + +type PathPoint = { + position: THREE.Vector3; + rotation: THREE.Quaternion; + uuid: string; +}; + +type PathCreatorProps = { + simulationPaths: PathPoint[][]; + setSimulationPaths: React.Dispatch>; + connections: { start: PathPoint; end: PathPoint }[]; + setConnections: React.Dispatch> +}; + +const PathCreator = ({ simulationPaths, setSimulationPaths, connections, setConnections }: PathCreatorProps) => { + const { camera, scene, raycaster, pointer, gl } = useThree(); + const { drawMaterialPath } = useDrawMaterialPath(); + + const [currentPath, setCurrentPath] = useState<{ position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string }[]>([]); + const [temporaryPoint, setTemporaryPoint] = useState(null); + const [selectedPoint, setSelectedPoint] = useState<{ position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string } | null>(null); + const [selectedConnectionPoint, setSelectedConnectionPoint] = useState<{ point: PathPoint; pathIndex: number } | null>(null); + const [previewConnection, setPreviewConnection] = useState<{ start: PathPoint; end?: THREE.Vector3 } | null>(null); + const [transformMode, setTransformMode] = useState<'translate' | 'rotate'>('translate'); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (selectedPoint) { + if (event.key === 'g') { + setTransformMode('translate'); + } else if (event.key === 'r') { + setTransformMode('rotate'); + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [selectedPoint]); + + useEffect(() => { + const canvasElement = gl.domElement; + + let drag = false; + let MouseDown = false; + + const onMouseDown = () => { + MouseDown = true; + drag = false; + }; + + const onMouseUp = () => { + MouseDown = false; + }; + + const onMouseMove = () => { + if (MouseDown) { + drag = true; + } + }; + + const onContextMenu = (e: any) => { + e.preventDefault(); + if (drag || e.button === 0) return; + if (currentPath.length > 1) { + setSimulationPaths((prevPaths) => [...prevPaths, currentPath]); + } + setCurrentPath([]); + setTemporaryPoint(null); + setPreviewConnection(null); + setSelectedConnectionPoint(null); + }; + + const onMouseClick = (evt: any) => { + if (drag || evt.button !== 0) return; + + evt.preventDefault(); + raycaster.setFromCamera(pointer, camera); + + let intersects = raycaster.intersectObjects(scene.children, true); + + if (intersects.some((intersect) => intersect.object.name.includes("path-point"))) { + intersects = []; + } else { + intersects = intersects.filter( + (intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.userData.isPathObject && + !(intersect.object.type === "GridHelper") + ); + } + + if (intersects.length > 0 && selectedPoint === null) { + let point = intersects[0].point; + if (point.y < 0.05) { + point = new THREE.Vector3(point.x, 0.05, point.z); + } + const newPoint = { + position: point, + rotation: new THREE.Quaternion(), + uuid: THREE.MathUtils.generateUUID(), + }; + setCurrentPath((prevPath) => [...prevPath, newPoint]); + setTemporaryPoint(null); + } else { + setSelectedPoint(null); + } + }; + + if (drawMaterialPath) { + canvasElement.addEventListener("mousedown", onMouseDown); + canvasElement.addEventListener("mouseup", onMouseUp); + canvasElement.addEventListener("mousemove", onMouseMove); + canvasElement.addEventListener("click", onMouseClick); + canvasElement.addEventListener("contextmenu", onContextMenu); + } else { + if (currentPath.length > 1) { + setSimulationPaths((prevPaths) => [...prevPaths, currentPath]); + } + setCurrentPath([]); + setTemporaryPoint(null); + } + + return () => { + canvasElement.removeEventListener("mousedown", onMouseDown); + canvasElement.removeEventListener("mouseup", onMouseUp); + canvasElement.removeEventListener("mousemove", onMouseMove); + canvasElement.removeEventListener("click", onMouseClick); + canvasElement.removeEventListener("contextmenu", onContextMenu); + }; + }, [camera, scene, raycaster, currentPath, drawMaterialPath, selectedPoint]); + + useFrame(() => { + if (drawMaterialPath && currentPath.length > 0) { + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObjects(scene.children, true).filter( + (intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.userData.isPathObject && + !(intersect.object.type === "GridHelper") + ); + + if (intersects.length > 0) { + let point = intersects[0].point; + if (point.y < 0.05) { + point = new THREE.Vector3(point.x, 0.05, point.z); + } + setTemporaryPoint(point); + } else { + setTemporaryPoint(null); + } + } else { + setTemporaryPoint(null); + } + }); + + const handlePointClick = (point: { position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string }) => { + if (currentPath.length === 0 && drawMaterialPath) { + setSelectedPoint(point); + } else { + setSelectedPoint(null); + } + }; + + const handleTransform = (e: any) => { + if (selectedPoint) { + const updatedPosition = e.target.object.position.clone(); + const updatedRotation = e.target.object.quaternion.clone(); + const updatedPaths = simulationPaths.map((path) => + path.map((p) => + p.uuid === selectedPoint.uuid ? { ...p, position: updatedPosition, rotation: updatedRotation } : p + ) + ); + setSimulationPaths(updatedPaths); + } + }; + + + const meshContext = (uuid: string) => { + const pathIndex = simulationPaths.findIndex(path => path.some(point => point.uuid === uuid)); + if (pathIndex === -1) return; + + const clickedPoint = simulationPaths[pathIndex].find(point => point.uuid === uuid); + if (!clickedPoint) return; + + const isStart = simulationPaths[pathIndex][0].uuid === uuid; + const isEnd = simulationPaths[pathIndex][simulationPaths[pathIndex].length - 1].uuid === uuid; + + if (pathIndex === 0 && isStart) { + console.log("The first-ever point is not connectable."); + setSelectedConnectionPoint(null); + setPreviewConnection(null); + return; + } + + if (!isStart && !isEnd) { + console.log("Selected point is not a valid connection point (not start or end)"); + setSelectedConnectionPoint(null); + setPreviewConnection(null); + return; + } + + if (connections.some(conn => conn.start.uuid === uuid || conn.end.uuid === uuid)) { + console.log("The selected point is already connected."); + setSelectedConnectionPoint(null); + setPreviewConnection(null); + return; + } + + if (!selectedConnectionPoint) { + setSelectedConnectionPoint({ point: clickedPoint, pathIndex }); + setPreviewConnection({ start: clickedPoint }); + console.log("First point selected for connection:", clickedPoint); + return; + } + + if (selectedConnectionPoint.pathIndex === pathIndex) { + console.log("Cannot connect points within the same path."); + setSelectedConnectionPoint(null); + setPreviewConnection(null); + return; + } + + if (connections.some(conn => conn.start.uuid === clickedPoint.uuid || conn.end.uuid === clickedPoint.uuid)) { + console.log("The target point is already connected."); + setSelectedConnectionPoint(null); + setPreviewConnection(null); + return; + } + + setConnections(prevConnections => [ + ...prevConnections, + { start: selectedConnectionPoint.point, end: clickedPoint }, + ]); + + + setSelectedConnectionPoint(null); + setPreviewConnection(null); + }; + + useEffect(() => { + if (!selectedConnectionPoint) { + setPreviewConnection(null); + } + }, [selectedConnectionPoint, connections]); + + useFrame(() => { + if (selectedConnectionPoint) { + raycaster.setFromCamera(pointer, camera); + + const intersects = raycaster.intersectObjects(scene.children, true).filter( + (intersect) => + !intersect.object.name.includes("Roof") && + !intersect.object.name.includes("MeasurementReference") && + !intersect.object.userData.isPathObject && + !(intersect.object.type === "GridHelper") + ); + + if (intersects.length > 0) { + let point = intersects[0].point; + if (point.y < 0.05) { + point = new THREE.Vector3(point.x, 0.05, point.z); + } + setPreviewConnection({ start: selectedConnectionPoint.point, end: point }); + } else { + setPreviewConnection(null); + } + } + }); + + return ( + <> + + {/* Render finalized simulationPaths */} + {simulationPaths.map((path, pathIndex) => ( + + point.position)} + color="yellow" + lineWidth={5} + userData={{ isPathObject: true }} + /> + + ))} + + {/* Render finalized points */} + {simulationPaths.map((path) => + path.map((point) => ( + handlePointClick(point)} + onPointerMissed={() => { setSelectedPoint(null) }} + onContextMenu={() => { meshContext(point.uuid); }} + > + + + + )) + )} + + {connections.map((conn, index) => ( + + ))} + + + + {/* Render current path */} + {currentPath.length > 1 && ( + + point.position)} + color="red" + lineWidth={5} + userData={{ isPathObject: true }} + /> + + )} + + {/* Render current path points */} + {currentPath.map((point) => ( + + + + + ))} + + {/* Render temporary indicator line */} + {temporaryPoint && currentPath.length > 0 && ( + + + + )} + + {/* Render dashed preview connection */} + {previewConnection && previewConnection.end && ( + + )} + + {/* Render temporary point */} + {temporaryPoint && ( + + + + + )} + + {/* Attach TransformControls to the selected point */} + {selectedPoint && ( + + )} + + ); +}; + +export default PathCreator; \ No newline at end of file diff --git a/app/src/modules/simulation/simulationtemp/path/pathFlow.tsx b/app/src/modules/simulation/simulationtemp/path/pathFlow.tsx new file mode 100644 index 0000000..1ec8ba8 --- /dev/null +++ b/app/src/modules/simulation/simulationtemp/path/pathFlow.tsx @@ -0,0 +1,164 @@ +import * as THREE from 'three'; +import { useState, useEffect, useRef, useMemo } from "react"; +import { useLoader, useFrame } from "@react-three/fiber"; +import { GLTFLoader } from "three-stdlib"; +import crate from "../../../../assets/models/gltf-glb/crate_box.glb"; +import { useOrganization } from '../../../../store/store'; +import { useControls } from 'leva'; + +type PathPoint = { + position: THREE.Vector3; + rotation: THREE.Quaternion; + uuid: string; +}; + +type PathFlowProps = { + path: PathPoint[]; + connections: { start: PathPoint; end: PathPoint }[]; +}; + +export default function PathFlow({ path, connections }: PathFlowProps) { + const { organization } = useOrganization(); + const [isPaused, setIsPaused] = useState(false); + const [isStopped, setIsStopped] = useState(false); + + const { spawnInterval, speed, pauseResume, startStop } = useControls({ + spawnInterval: { value: 1000, min: 500, max: 5000, step: 100 }, + speed: { value: 2, min: 1, max: 20, step: 0.5 }, + pauseResume: { value: false, label: "Pause/Resume" }, + startStop: { value: false, label: "Start/Stop" }, + }); + + const [meshes, setMeshes] = useState<{ id: number }[]>([]); + const gltf = useLoader(GLTFLoader, crate); + + const meshIdRef = useRef(0); + const lastSpawnTime = useRef(performance.now()); + const totalPausedTime = useRef(0); + const pauseStartTime = useRef(null); + + useEffect(() => { + setIsPaused(pauseResume); + setIsStopped(startStop); + }, [pauseResume, startStop]); + + const removeMesh = (id: number) => { + setMeshes((prev) => prev.filter((m) => m.id !== id)); + }; + + useFrame(() => { + if (organization !== 'hexrfactory' || isStopped || !path) return; + + const now = performance.now(); + + if (isPaused) { + if (pauseStartTime.current === null) { + pauseStartTime.current = now; + } + return; + } + + if (pauseStartTime.current !== null) { + totalPausedTime.current += now - pauseStartTime.current; + pauseStartTime.current = null; + } + + const adjustedTime = now - totalPausedTime.current; + + if (adjustedTime - lastSpawnTime.current >= spawnInterval) { + setMeshes((prev) => [...prev, { id: meshIdRef.current++ }]); + lastSpawnTime.current = adjustedTime; + } + }); + + return ( + <> + {meshes.map((mesh) => ( + + ))} + + ); +} + +function MovingMesh({ meshId, points, speed, gltf, removeMesh, isPaused }: any) { + const meshRef = useRef(); + const startTime = useRef(null); // Initialize as null + const pausedTime = useRef(0); + const pauseStartTime = useRef(null); + + const distances = useMemo(() => { + if (!points || points.length < 2) return []; + return points.slice(1).map((point: any, i: number) => points[i].position.distanceTo(point.position)); + }, [points]); + + useFrame(() => { + if (!points || points.length < 2) return; + + if (startTime.current === null && points.length > 0) { + startTime.current = performance.now(); + } + + if (!meshRef.current) return; + + if (isPaused) { + if (pauseStartTime.current === null) { + pauseStartTime.current = performance.now(); + } + return; + } + + if (pauseStartTime.current !== null) { + pausedTime.current += performance.now() - pauseStartTime.current; + pauseStartTime.current = null; + } + + if (startTime.current === null) return; + + const elapsed = performance.now() - startTime.current - pausedTime.current; + + const distanceTraveled = elapsed / 1000 * speed; + + let remainingDistance = distanceTraveled; + let currentSegmentIndex = 0; + + while (currentSegmentIndex < distances.length && remainingDistance > distances[currentSegmentIndex]) { + remainingDistance -= distances[currentSegmentIndex]; + currentSegmentIndex++; + } + + if (currentSegmentIndex >= distances.length) { + removeMesh(meshId); + return; + } + + const progress = remainingDistance / distances[currentSegmentIndex]; + const start = points[currentSegmentIndex].position; + const end = points[currentSegmentIndex + 1].position; + + meshRef.current.position.lerpVectors(start, end, Math.min(progress, 1)); + + const startRotation = points[currentSegmentIndex].rotation; + const endRotation = points[currentSegmentIndex + 1].rotation; + const interpolatedRotation = new THREE.Quaternion().slerpQuaternions(startRotation, endRotation, Math.min(progress, 1)); + + meshRef.current.quaternion.copy(interpolatedRotation); + }); + + return ( + <> + {points && points.length > 0 && + + + + } + + ); +} \ No newline at end of file diff --git a/app/src/modules/simulation/simulationtemp/process/processCreator.tsx b/app/src/modules/simulation/simulationtemp/process/processCreator.tsx new file mode 100644 index 0000000..52ac683 --- /dev/null +++ b/app/src/modules/simulation/simulationtemp/process/processCreator.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +function ProcessCreator() { + return ( + <> + ) +} + +export default ProcessCreator \ No newline at end of file diff --git a/app/src/modules/simulation/simulationtemp/simulation.tsx b/app/src/modules/simulation/simulationtemp/simulation.tsx new file mode 100644 index 0000000..828fa03 --- /dev/null +++ b/app/src/modules/simulation/simulationtemp/simulation.tsx @@ -0,0 +1,26 @@ +import React, { useState } from 'react'; +import * as THREE from 'three'; +import PathCreator from './path/pathCreator'; +import PathFlow from './path/pathFlow'; + +type PathPoint = { + position: THREE.Vector3; + rotation: THREE.Quaternion; + uuid: string; +}; + +function Simulation() { + const [simulationPaths, setSimulationPaths] = useState<{ position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string }[][]>([]); + const [connections, setConnections] = useState<{ start: PathPoint; end: PathPoint }[]>([]); + + return ( + <> + + {simulationPaths.map((path, index) => ( + + ))} + + ); +} + +export default Simulation; \ No newline at end of file diff --git a/app/src/pages/Project.tsx b/app/src/pages/Project.tsx index 16c0461..a1af9db 100644 --- a/app/src/pages/Project.tsx +++ b/app/src/pages/Project.tsx @@ -1,14 +1,42 @@ -import React from "react"; +import React, { useEffect } from "react"; import ModuleToggle from "../components/ui/ModuleToggle"; import SideBarLeft from "../components/layout/sidebarLeft/SideBarLeft"; import SideBarRight from "../components/layout/sidebarRight/SideBarRight"; import useModuleStore from "../store/useModuleStore"; import RealTimeVisulization from "../components/ui/componets/RealTimeVisulization"; import Tools from "../components/ui/Tools"; +import Scene from "../modules/scene/scene"; +import { useSocketStore, useFloorItems, useOrganization, useUserName, useWallItems, useZones } from "../store/store"; +import { useNavigate } from "react-router-dom"; const Project: React.FC = () => { + let navigate = useNavigate(); const { activeModule } = useModuleStore(); + const { userName, setUserName } = useUserName(); + const { organization, setOrganization } = useOrganization(); + const { setFloorItems } = useFloorItems(); + const { setWallItems } = useWallItems(); + const { setZones } = useZones(); + + useEffect(() => { + setFloorItems([]); + setWallItems([]); + setZones([]); + const email = localStorage.getItem('email') + if (email) { + useSocketStore.getState().initializeSocket(email); + const Organization = (email!.split("@")[1]).split(".")[0]; + const name = localStorage.getItem('userName'); + if (Organization && name) { + setOrganization(Organization); + setUserName(name); + } + } else { + navigate("/"); + } + }, []) + return (
@@ -16,6 +44,7 @@ const Project: React.FC = () => { {activeModule === "visualization" && } +
); }; diff --git a/app/src/services/factoryBuilder/assest/assets/getAssetImages.ts b/app/src/services/factoryBuilder/assest/assets/getAssetImages.ts new file mode 100644 index 0000000..9d41601 --- /dev/null +++ b/app/src/services/factoryBuilder/assest/assets/getAssetImages.ts @@ -0,0 +1,23 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + +export const getAssetImages = async (cursor?: string) => { + try { + const response = await fetch( + `${url_Backend_dwinzo}/api/v3/AssetDatas?limit=10${cursor ? `&cursor=${cursor}` : ""}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error("Failed to fetch assets"); + } + + return await response.json(); + } catch (error: any) { + throw new Error(error.message); + } +}; diff --git a/app/src/services/factoryBuilder/assest/assets/getAssetModel.ts b/app/src/services/factoryBuilder/assest/assets/getAssetModel.ts new file mode 100644 index 0000000..cf1ed5a --- /dev/null +++ b/app/src/services/factoryBuilder/assest/assets/getAssetModel.ts @@ -0,0 +1,25 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + +export const getAssetModel = async (modelId: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/AssetFile/${modelId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch model"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/assest/floorAsset/deleteFloorItemApi.ts b/app/src/services/factoryBuilder/assest/floorAsset/deleteFloorItemApi.ts new file mode 100644 index 0000000..ecd7a54 --- /dev/null +++ b/app/src/services/factoryBuilder/assest/floorAsset/deleteFloorItemApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const deleteFloorItem = async (organization: string, modeluuid: string, modelname: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/deletefloorItem`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, modeluuid, modelname }), + }); + + if (!response.ok) { + throw new Error("Failed to delete Floor Item"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/assest/floorAsset/getFloorItemsApi.ts b/app/src/services/factoryBuilder/assest/floorAsset/getFloorItemsApi.ts new file mode 100644 index 0000000..ad315e7 --- /dev/null +++ b/app/src/services/factoryBuilder/assest/floorAsset/getFloorItemsApi.ts @@ -0,0 +1,25 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const getFloorItems = async (organization: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/findfloorItems/${organization}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to get Floor Items"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/assest/floorAsset/setFloorItemApi.ts b/app/src/services/factoryBuilder/assest/floorAsset/setFloorItemApi.ts new file mode 100644 index 0000000..a4fe765 --- /dev/null +++ b/app/src/services/factoryBuilder/assest/floorAsset/setFloorItemApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const setFloorItemApi = async (organization: string, modeluuid: string, modelname: string, position: Object, rotation: Object, modelfileID: string, isLocked: boolean, isVisible: boolean) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/setFloorItems`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, modeluuid, modelname, position, rotation, modelfileID, isLocked, isVisible }), + }); + + if (!response.ok) { + throw new Error("Failed to set or update Floor Item"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/assest/wallAsset/deleteWallItemApi.ts b/app/src/services/factoryBuilder/assest/wallAsset/deleteWallItemApi.ts new file mode 100644 index 0000000..5dda37c --- /dev/null +++ b/app/src/services/factoryBuilder/assest/wallAsset/deleteWallItemApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const deleteWallItem = async (organization: string, modeluuid: string, modelname: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/deleteWallItem`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, modeluuid, modelname }), + }); + + if (!response.ok) { + throw new Error("Failed to delete Wall Item"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/assest/wallAsset/getWallItemsApi.ts b/app/src/services/factoryBuilder/assest/wallAsset/getWallItemsApi.ts new file mode 100644 index 0000000..eb0a232 --- /dev/null +++ b/app/src/services/factoryBuilder/assest/wallAsset/getWallItemsApi.ts @@ -0,0 +1,25 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const getWallItems = async (organization: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/findWallItems/${organization}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to get Wall Items"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/assest/wallAsset/setWallItemApi.ts b/app/src/services/factoryBuilder/assest/wallAsset/setWallItemApi.ts new file mode 100644 index 0000000..e51297b --- /dev/null +++ b/app/src/services/factoryBuilder/assest/wallAsset/setWallItemApi.ts @@ -0,0 +1,36 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const setWallItem = async ( + organization: string, + modeluuid: string, + modelname: string, + type: string, + csgposition: Object, + csgscale: Object, + position: Object, + quaternion: Object, + scale: Object +) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/setWallItems`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, modeluuid, modelname, position, type, csgposition, csgscale, quaternion, scale }), + }); + + if (!response.ok) { + throw new Error("Failed to set or update Wall Item"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/camera/getCameraApi.ts b/app/src/services/factoryBuilder/camera/getCameraApi.ts new file mode 100644 index 0000000..e8d84d9 --- /dev/null +++ b/app/src/services/factoryBuilder/camera/getCameraApi.ts @@ -0,0 +1,32 @@ +import { setCamera } from './setCameraApi'; +import * as THREE from 'three'; + +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const getCamera = async (organization: string, userId: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/getCamera/${organization}/${userId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to get Camera position and target"); + } + + const result = await response.json(); + if (result === "user not found") { + return null; + } else { + return result; + } + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/camera/setCameraApi.ts b/app/src/services/factoryBuilder/camera/setCameraApi.ts new file mode 100644 index 0000000..46d30e3 --- /dev/null +++ b/app/src/services/factoryBuilder/camera/setCameraApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const setCamera = async (organization: string, userId: string, position: Object, target: Object, rotation: Object) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/setCamera`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, userId, position, target, rotation }), + }); + + if (!response.ok) { + throw new Error("Failed to set Camera Position and Target"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/collab/getActiveUsers.ts b/app/src/services/factoryBuilder/collab/getActiveUsers.ts new file mode 100644 index 0000000..30242b6 --- /dev/null +++ b/app/src/services/factoryBuilder/collab/getActiveUsers.ts @@ -0,0 +1,32 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export default async function getActiveUsersData(organization: string) { + const apiUrl = `${url_Backend_dwinzo}/api/v1/activeCameras/${organization}`; + + try { + const response = await fetch(apiUrl, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status} - ${response.statusText}`); + } + + + if (!response.ok) { + throw new Error("Failed to get active cameras "); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/collab/getUsersApi.ts b/app/src/services/factoryBuilder/collab/getUsersApi.ts new file mode 100644 index 0000000..808eb76 --- /dev/null +++ b/app/src/services/factoryBuilder/collab/getUsersApi.ts @@ -0,0 +1,32 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export default async function fetchShareUsers(organization: string) { + const apiUrl = `${url_Backend_dwinzo}/api/v1/findshareUsers?organization=${organization}`; + + try { + const response = await fetch(apiUrl, { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }); + + if (!response.ok) { + throw new Error(`Error: ${response.status} - ${response.statusText}`); + } + + + if (!response.ok) { + throw new Error("Failed to get users "); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/collab/giveCollabAccess.ts b/app/src/services/factoryBuilder/collab/giveCollabAccess.ts new file mode 100644 index 0000000..b70b4f7 --- /dev/null +++ b/app/src/services/factoryBuilder/collab/giveCollabAccess.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export default async function giveCollabAccess(email: string, isShare: boolean, organization: string) { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/shareUser`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, isShare, organization }), + }); + + if (!response.ok) { + throw new Error("Failed to set Camera Position and Target"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/environment/findEnvironment.ts b/app/src/services/factoryBuilder/environment/findEnvironment.ts new file mode 100644 index 0000000..ff81b07 --- /dev/null +++ b/app/src/services/factoryBuilder/environment/findEnvironment.ts @@ -0,0 +1,32 @@ +import { setEnvironment } from './setEnvironment'; + +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const findEnvironment = async (organization: string, userId: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/findEnvironments/${organization}/${userId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to get wall and roof visibility"); + } + + const result = await response.json(); + if (result === "user not found") { + const userpos = setEnvironment(organization, userId, false, false, false); + return userpos; + } else { + return result; + } + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/environment/setEnvironment.ts b/app/src/services/factoryBuilder/environment/setEnvironment.ts new file mode 100644 index 0000000..072b7bb --- /dev/null +++ b/app/src/services/factoryBuilder/environment/setEnvironment.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const setEnvironment = async (organization: string, userId: string, wallVisibility: Boolean, roofVisibility: Boolean, shadowVisibility: Boolean) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/setEvironments`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, userId, wallVisibility, roofVisibility, shadowVisibility }), + }); + + if (!response.ok) { + throw new Error("Failed to set wall and roof visibility"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/lines/deleteLayerApi.ts b/app/src/services/factoryBuilder/lines/deleteLayerApi.ts new file mode 100644 index 0000000..54160c8 --- /dev/null +++ b/app/src/services/factoryBuilder/lines/deleteLayerApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const deleteLayer = async (organization: string, layer: number) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/deleteLayer`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, layer }), + }); + + if (!response.ok) { + throw new Error("Failed to delete line"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/lines/deleteLineApi.ts b/app/src/services/factoryBuilder/lines/deleteLineApi.ts new file mode 100644 index 0000000..64a76c4 --- /dev/null +++ b/app/src/services/factoryBuilder/lines/deleteLineApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const deleteLineApi = async (organization: string, line: Object) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/deleteLine`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, line }), + }); + + if (!response.ok) { + throw new Error("Failed to delete line"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/lines/deletePointApi.ts b/app/src/services/factoryBuilder/lines/deletePointApi.ts new file mode 100644 index 0000000..60a6fd4 --- /dev/null +++ b/app/src/services/factoryBuilder/lines/deletePointApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const deletePointApi = async (organization: string, uuid: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/deletePoint`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, uuid }), + }); + + if (!response.ok) { + throw new Error("Failed to delete point"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/lines/getLinesApi.ts b/app/src/services/factoryBuilder/lines/getLinesApi.ts new file mode 100644 index 0000000..00c86a9 --- /dev/null +++ b/app/src/services/factoryBuilder/lines/getLinesApi.ts @@ -0,0 +1,25 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const getLines = async (organization: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/findLines/${organization}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to get Lines"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/lines/setLineApi.ts b/app/src/services/factoryBuilder/lines/setLineApi.ts new file mode 100644 index 0000000..269c26d --- /dev/null +++ b/app/src/services/factoryBuilder/lines/setLineApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const setLine = async (organization: string, layer: number, line: Object, type: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/setLine`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, layer, line, type }), + }); + + if (!response.ok) { + throw new Error("Failed to set line"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/lines/updatePointApi.ts b/app/src/services/factoryBuilder/lines/updatePointApi.ts new file mode 100644 index 0000000..8e4a93a --- /dev/null +++ b/app/src/services/factoryBuilder/lines/updatePointApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const updatePoint = async (organization: string, position: Object, uuid: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/updatePoint`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ organization, position, uuid }), + }); + + if (!response.ok) { + throw new Error("Failed to update point"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/mqtt/mqttEvents.ts b/app/src/services/factoryBuilder/mqtt/mqttEvents.ts new file mode 100644 index 0000000..fc009be --- /dev/null +++ b/app/src/services/factoryBuilder/mqtt/mqttEvents.ts @@ -0,0 +1,48 @@ +import React, { useEffect } from "react"; +import mqtt from "mqtt"; +import { useDrieUIValue } from "../../../store/store"; + +const MqttEvents = () => { + const { setTouch, setTemperature, setHumidity } = useDrieUIValue(); + useEffect(() => { + + const client = mqtt.connect("ws://192.168.0.192:1884", { + username: "gabby", + password: "gabby" + }); + + client.subscribe("touch"); + client.subscribe("temperature"); + client.subscribe("humidity"); + + const handleMessage = (topic: string, message: any) => { + const value = message.toString(); + + if (topic === "touch") { + setTouch(value); + } else if (topic === "temperature") { + setTemperature(parseFloat(value)); + } else if (topic === "humidity") { + setHumidity(parseFloat(value)); + } + }; + + client.on("message", handleMessage); + + client.on("error", (err) => { + console.error("MQTT Connection Error:", err); + }); + + client.on("close", () => { + console.log("MQTT Connection Closed"); + }); + + return () => { + client.end(); + }; + }, [setTouch, setTemperature, setHumidity]); + + return null; +}; + +export default MqttEvents; diff --git a/app/src/services/factoryBuilder/signInSignUp/signInApi.ts b/app/src/services/factoryBuilder/signInSignUp/signInApi.ts new file mode 100644 index 0000000..be608c0 --- /dev/null +++ b/app/src/services/factoryBuilder/signInSignUp/signInApi.ts @@ -0,0 +1,22 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const signIn = async (email: string, password: Object, organization: Object) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password, organization }), + }); + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + return { error: error.message }; + } else { + return { error: "An unknown error occurred" }; + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/signInSignUp/signUpApi.ts b/app/src/services/factoryBuilder/signInSignUp/signUpApi.ts new file mode 100644 index 0000000..87ded2b --- /dev/null +++ b/app/src/services/factoryBuilder/signInSignUp/signUpApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const signUp = async (userName: string, email: string, password: Object, organization: Object) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/signup`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userName, email, password, organization }), + }); + + if (!response.ok) { + throw new Error("Failed to signUp"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/webWorkers/assetManagerWorker.js b/app/src/services/factoryBuilder/webWorkers/assetManagerWorker.js new file mode 100644 index 0000000..db17487 --- /dev/null +++ b/app/src/services/factoryBuilder/webWorkers/assetManagerWorker.js @@ -0,0 +1,51 @@ +import * as THREE from 'three'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/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); + +onmessage = (event) => { + const { floorItems, cameraPosition, uuids, renderDistance } = event.data; + if (!floorItems) return + + const toAdd = []; + const toRemove = []; + + const cameraPos = new THREE.Vector3(cameraPosition.x, cameraPosition.y, cameraPosition.z); + + // Check for items to be added + floorItems.forEach((item) => { + const itemPosition = new THREE.Vector3(...item.position); + const distance = cameraPos.distanceTo(itemPosition); + + if (distance <= renderDistance && !uuids.includes(item.modeluuid)) { + toAdd.push(item); + } + }); + + // Sort the toAdd array based on distance (closest first) + toAdd.sort((a, b) => { + const aDistance = cameraPos.distanceTo(new THREE.Vector3(...a.position)); + const bDistance = cameraPos.distanceTo(new THREE.Vector3(...b.position)); + return aDistance - bDistance; + }); + + // Check for items to be removed + uuids.forEach((uuid) => { + const floorItem = floorItems.find((item) => item.modeluuid === uuid); + if (floorItem) { + const itemPosition = new THREE.Vector3(...floorItem.position); + const distance = cameraPos.distanceTo(itemPosition); + + if (distance > renderDistance) { + toRemove.push(uuid); + } + } + }); + + // Send the result back to the main thread + postMessage({ toAdd, toRemove }); +}; diff --git a/app/src/services/factoryBuilder/webWorkers/gltfLoaderWorker.js b/app/src/services/factoryBuilder/webWorkers/gltfLoaderWorker.js new file mode 100644 index 0000000..a051538 --- /dev/null +++ b/app/src/services/factoryBuilder/webWorkers/gltfLoaderWorker.js @@ -0,0 +1,38 @@ +import * as THREE from 'three'; +import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'; +import { retrieveGLTF, storeGLTF } from '../../../components/scene/indexDB/idbUtils'; + +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); +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_MARKETPLACE_URL}`; + +onmessage = async (event) => { + const { floorItems } = event.data; + + const uniqueItems = floorItems.filter((item, index, self) => + index === self.findIndex((t) => t.modelfileID === item.modelfileID) + ); + + for (const item of uniqueItems) { + const modelID = item.modelfileID; + const indexedDBModel = await retrieveGLTF(modelID); + + let modelBlob; + if (indexedDBModel) { + modelBlob = indexedDBModel; + const message = "gltfLoaded"; + postMessage({ message, modelID, modelBlob }); + } else { + const modelUrl = `${url_Backend_dwinzo}/api/v1/AssetFile/${modelID}`; + const modelBlob = await fetch(modelUrl).then((res) => res.blob()); + await storeGLTF(modelID, modelBlob); + const message = "gltfLoaded"; + postMessage({ message, modelID, modelBlob }); + } + } + + postMessage({ message: 'done' }) +}; diff --git a/app/src/services/factoryBuilder/webWorkers/shadowWorker.js b/app/src/services/factoryBuilder/webWorkers/shadowWorker.js new file mode 100644 index 0000000..67bd52a --- /dev/null +++ b/app/src/services/factoryBuilder/webWorkers/shadowWorker.js @@ -0,0 +1,11 @@ +import * as THREE from 'three'; + +onmessage = (event) => { + const { controlsTarget, sunPosition, offsetDistance } = event.data; + + const lightPosition = new THREE.Vector3() + .copy(controlsTarget) + .addScaledVector(new THREE.Vector3().copy(sunPosition).normalize(), offsetDistance); + + postMessage({ lightPosition, controlsTarget }); +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/zones/deleteZoneApi.ts b/app/src/services/factoryBuilder/zones/deleteZoneApi.ts new file mode 100644 index 0000000..25f7fcf --- /dev/null +++ b/app/src/services/factoryBuilder/zones/deleteZoneApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const deleteZonesApi = async (userId: string, organization: string, zoneId: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/setLine`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userId, organization, zoneId }), + }); + + if (!response.ok) { + throw new Error("Failed to delete zone"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/zones/getZonesApi.ts b/app/src/services/factoryBuilder/zones/getZonesApi.ts new file mode 100644 index 0000000..ef16874 --- /dev/null +++ b/app/src/services/factoryBuilder/zones/getZonesApi.ts @@ -0,0 +1,25 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const getZonesApi = async (organization: string) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v2/findZones/${organization}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + // if (!response.ok) { + // throw new Error("Failed to get Zones"); + // } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/services/factoryBuilder/zones/setZonesApi.ts b/app/src/services/factoryBuilder/zones/setZonesApi.ts new file mode 100644 index 0000000..a1fd3ed --- /dev/null +++ b/app/src/services/factoryBuilder/zones/setZonesApi.ts @@ -0,0 +1,26 @@ +let url_Backend_dwinzo = `http://${process.env.REACT_APP_SERVER_REST_API_BASE_URL}`; + +export const setZonesApi = async (userId: string, organization: string, zoneData: any) => { + try { + const response = await fetch(`${url_Backend_dwinzo}/api/v1/setLine`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userId, organization, zoneData }), + }); + + if (!response.ok) { + throw new Error("Failed to set zone"); + } + + const result = await response.json(); + return result; + } catch (error) { + if (error instanceof Error) { + throw new Error(error.message); + } else { + throw new Error("An unknown error occurred"); + } + } +}; \ No newline at end of file diff --git a/app/src/store/store.ts b/app/src/store/store.ts new file mode 100644 index 0000000..f14b808 --- /dev/null +++ b/app/src/store/store.ts @@ -0,0 +1,336 @@ +import * as THREE from "three"; +import * as Types from '../types/world/worldTypes'; +import { create } from "zustand"; +import { io } from "socket.io-client"; + +export const useSocketStore = create((set: any, get: any) => ({ + socket: null, + initializeSocket: (email: any) => { + const existingSocket = get().socket; + if (existingSocket) { + return; + } + + const socket = io(`http://${process.env.REACT_APP_SERVER_SOCKET_API_BASE_URL}/`, { + reconnection: false, + auth: { email } + }); + + set({ socket }); + }, + disconnectSocket: () => { + set((state: any) => { + state.socket?.disconnect(); + return { socket: null }; + }); + } +})); + +export const useOrganization = create((set: any) => ({ + organization: "", + setOrganization: (x: any) => set(() => ({ organization: x })), +})); + +export const useToggleView = create((set: any) => ({ + toggleView: false, + setToggleView: (x: any) => set(() => ({ toggleView: x })), +})); + +export const useUpdateScene = create((set: any) => ({ + updateScene: false, + setUpdateScene: (x: any) => set(() => ({ updateScene: x })), +})); + +export const useWalls = create((set: any) => ({ + walls: [], + setWalls: (x: any) => set(() => ({ walls: x })), +})); + +export const useZones = create((set: any) => ({ + zones: [], + setZones: (x: any) => set(() => ({ zones: x })), +})); + +interface ZonePointsState { + zonePoints: THREE.Vector3[]; + setZonePoints: (points: THREE.Vector3[]) => void; +} + +export const useZonePoints = create((set) => ({ + zonePoints: [], + setZonePoints: (points) => set({ zonePoints: points }), +})); + +export const useSelectedItem = create((set: any) => ({ + selectedItem: { name: "", id: "" }, + setSelectedItem: (x: any) => set(() => ({ selectedItem: x })), +})); + +export const useSelectedAssets = create((set: any) => ({ + selectedAssets: [], + setSelectedAssets: (x: any) => set(() => ({ selectedAssets: x })), +})); + +export const useLayers = create((set: any) => ({ + Layers: 1, + setLayers: (x: any) => set(() => ({ Layers: x })), +})); + +export const useCamPosition = create((set: any) => ({ + camPosition: { x: undefined, y: undefined, z: undefined }, + setCamPosition: (newCamPosition: any) => set({ camPosition: newCamPosition }), +})); + +export const useMenuVisible = create((set: any) => ({ + menuVisible: false, + setMenuVisible: (x: any) => set(() => ({ menuVisible: x })), +})); + +export const useDeleteModels = create((set: any) => ({ + deleteModels: false, + setDeleteModels: (x: any) => set(() => ({ deleteModels: x })), +})); + +export const useToolMode = create((set: any) => ({ + toolMode: null, + setToolMode: (x: any) => set(() => ({ toolMode: x })), +})); + +export const useNewLines = create((set: any) => ({ + newLines: [], + setNewLines: (x: any) => set(() => ({ newLines: x })), +})); + +export const useDeletedLines = create((set: any) => ({ + deletedLines: [], + setDeletedLines: (x: any) => set(() => ({ deletedLines: x })), +})); + +export const useMovePoint = create((set: any) => ({ + movePoint: false, + setMovePoint: (x: any) => set(() => ({ movePoint: x })), +})); + +export const useTransformMode = create((set: any) => ({ + transformMode: null, + setTransformMode: (x: any) => set(() => ({ transformMode: x })), +})); + +export const useDeletePointOrLine = create((set: any) => ({ + deletePointOrLine: false, + setDeletePointOrLine: (x: any) => set(() => ({ deletePointOrLine: x })), +})); + +export const useFloorItems = create((set: any) => ({ + floorItems: null, + setFloorItems: (callback: any) => + set((state: any) => ({ + floorItems: + typeof callback === "function" + ? callback(state.floorItems) + : callback, + })), +})); + +export const useWallItems = create((set: any) => ({ + wallItems: [], + setWallItems: (callback: any) => + set((state: any) => ({ + wallItems: + typeof callback === "function" + ? callback(state.wallItems) + : callback, + })), +})); + +export const useSelectedWallItem = create((set: any) => ({ + selectedWallItem: null, + setSelectedWallItem: (x: any) => set(() => ({ selectedWallItem: x })), +})); + +export const useselectedFloorItem = create((set: any) => ({ + selectedFloorItem: null, + setselectedFloorItem: (x: any) => set(() => ({ selectedFloorItem: x })), +})); + +export const useDeletableFloorItem = create((set: any) => ({ + deletableFloorItem: null, + setDeletableFloorItem: (x: any) => set(() => ({ deletableFloorItem: x })), +})); + +export const useSetScale = create((set: any) => ({ + scale: null, + setScale: (x: any) => set(() => ({ scale: x })), +})); + +export const useRoofVisibility = create((set: any) => ({ + roofVisibility: false, + setRoofVisibility: (x: any) => set(() => ({ roofVisibility: x })), +})); + +export const useWallVisibility = create((set: any) => ({ + wallVisibility: false, + setWallVisibility: (x: any) => set(() => ({ wallVisibility: x })), +})); + +export const useShadows = create((set: any) => ({ + shadows: false, + setShadows: (x: any) => set(() => ({ shadows: x })), +})); + +export const useSunPosition = create((set: any) => ({ + sunPosition: { x: undefined, y: undefined, z: undefined }, + setSunPosition: (newSuntPosition: any) => set({ sunPosition: newSuntPosition }), +})); + +export const useRemoveLayer = create((set: any) => ({ + removeLayer: false, + setRemoveLayer: (x: any) => set(() => ({ removeLayer: x })), +})); + +export const useRemovedLayer = create((set: any) => ({ + removedLayer: null, + setRemovedLayer: (x: any) => set(() => ({ removedLayer: x })), +})); + +export const useActiveLayer = create((set: any) => ({ + activeLayer: 1, + setActiveLayer: (x: any) => set({ activeLayer: x }), +})); + +export const useResetCamera = create((set: any) => ({ + resetCamera: false, + setResetCamera: (x: any) => set({ resetCamera: x }), +})); + +export const useAddAction = create((set: any) => ({ + addAction: null, + setAddAction: (x: any) => set({ addAction: x }), +})); + +export const useActiveTool = create((set: any) => ({ + activeTool: "Cursor", + setActiveTool: (x: any) => set({ activeTool: x }), +})); + +export const use2DUndoRedo = create((set: any) => ({ + is2DUndoRedo: null, + set2DUndoRedo: (x: any) => set({ is2DUndoRedo: x }), +})) + +export const useElevation = create((set: any) => ({ + elevation: 45, + setElevation: (x: any) => set({ elevation: x }), +})); + +export const useAzimuth = create((set: any) => ({ + azimuth: -160, + setAzimuth: (x: any) => set({ azimuth: x }), +})); + +export const useRenderDistance = create((set: any) => ({ + renderDistance: 50, + setRenderDistance: (x: any) => set({ renderDistance: x }), +})); + +export const useCamMode = create((set: any) => ({ + camMode: "ThirdPerson", + setCamMode: (x: any) => set({ camMode: x }), +})); + +export const useUserName = create((set: any) => ({ + userName: "", + setUserName: (x: any) => set({ userName: x }), +})); + +export const useObjectPosition = create((set: any) => ({ + objectPosition: { x: undefined, y: undefined, z: undefined }, + setObjectPosition: (newObjectPosition: any) => set({ objectPosition: newObjectPosition }), +})); + +export const useObjectScale = create((set: any) => ({ + objectScale: { x: undefined, y: undefined, z: undefined }, + setObjectScale: (newObjectScale: any) => set({ objectScale: newObjectScale }), +})); + +export const useObjectRotation = create((set: any) => ({ + objectRotation: { x: undefined, y: undefined, z: undefined }, + setObjectRotation: (newObjectRotation: any) => set({ objectRotation: newObjectRotation }), +})); + +export const useDrieTemp = create((set: any) => ({ + drieTemp: undefined, + setDrieTemp: (x: any) => set({ drieTemp: x }), +})); + +export const useActiveUsers = create((set: any) => ({ + activeUsers: [], + setActiveUsers: (x: any) => set({ activeUsers: x }), +})); + +export const useDrieUIValue = create((set: any) => ({ + drieUIValue: { touch: null, temperature: null, humidity: null }, + + setDrieUIValue: (x: any) => set((state: any) => ({ drieUIValue: { ...state.drieUIValue, ...x } })), + + setTouch: (value: any) => set((state: any) => ({ drieUIValue: { ...state.drieUIValue, touch: value } })), + setTemperature: (value: any) => set((state: any) => ({ drieUIValue: { ...state.drieUIValue, temperature: value } })), + setHumidity: (value: any) => set((state: any) => ({ drieUIValue: { ...state.drieUIValue, humidity: value } })), +})); + +export const useDrawMaterialPath = create((set: any) => ({ + drawMaterialPath: false, + setDrawMaterialPath: (x: any) => set({ drawMaterialPath: x }), +})); + +export const useSelectedEventSphere = create((set: any) => ({ + selectedEventSphere: undefined, + setSelectedEventSphere: (x: any) => set({ selectedEventSphere: x }), +})); + +export const useSelectedPath = create((set: any) => ({ + selectedPath: undefined, + setSelectedPath: (x: any) => set({ selectedPath: x }), +})); + +export const useSimulationPaths = create((set) => ({ + simulationPaths: [], + setSimulationPaths: (paths) => set({ simulationPaths: paths }), +})); + +export const useConnections = create((set) => ({ + connections: [], + + setConnections: (connections) => set({ connections }), + + addConnection: (newConnection) => + set((state) => ({ + connections: [...state.connections, newConnection], + })), + + removeConnection: (fromUUID, toUUID) => + set((state) => ({ + connections: state.connections + .map((connection) => + connection.fromUUID === fromUUID + ? { + ...connection, + toConnections: connection.toConnections.filter( + (to) => to.toUUID !== toUUID + ), + } + : connection + ) + .filter((connection) => connection.toConnections.length > 0), + })), +})); + +export const useIsConnecting = create((set: any) => ({ + isConnecting: false, + setIsConnecting: (x: any) => set({ isConnecting: x }), +})); + +export const useStartSimulation = create((set: any) => ({ + startSimulation: false, + setStartSimulation: (x: any) => set({ startSimulation: x }), +})); \ No newline at end of file diff --git a/app/src/styles/abstracts/variables.scss b/app/src/styles/abstracts/variables.scss index f36e2dd..31a9cba 100644 --- a/app/src/styles/abstracts/variables.scss +++ b/app/src/styles/abstracts/variables.scss @@ -71,7 +71,7 @@ $font-roboto: "Roboto", sans-serif; // Roboto font // Font sizes (converted to rem using a utility function) $tiny: 0.625rem; // Extra small text (10px) $small: 0.75rem; // Small text (12px) -$regular: 0.875rem; // Default text size (14px) +$regular: 0.8rem; // Default text size (14px) $large: 1rem; // Large text size (16px) $xlarge: 1.125rem; // Extra large text size (18px) $xxlarge: 1.5rem; // Double extra large text size (24px) @@ -89,6 +89,10 @@ $bold-weight: 600; // Bold font weight // Z-index variables for layering $z-index-drei-html: 1; // For drei's Html components +$z-index-default: 1; // For drei's Html components +$z-index-marketplace: 2; // For drei's Html components +$z-index-tools: 3; // For drei's Html components +$z-index-negative: -1; // For drei's Html components $z-index-ui-base: 10; // Base UI elements $z-index-ui-overlay: 20; // Overlay UI elements (e.g., modals, tooltips) $z-index-ui-popup: 30; // Popups, dialogs, or higher-priority UI elements diff --git a/app/src/styles/components/moduleToggle.scss b/app/src/styles/components/moduleToggle.scss index 60ed0cc..a6a77c8 100644 --- a/app/src/styles/components/moduleToggle.scss +++ b/app/src/styles/components/moduleToggle.scss @@ -10,6 +10,7 @@ left: 50%; top: 32px; transform: translateX(-50%); + z-index: #{$z-index-tools}; .module-list { display: flex; align-items: center; diff --git a/app/src/styles/components/tools.scss b/app/src/styles/components/tools.scss index 46791d7..166f041 100644 --- a/app/src/styles/components/tools.scss +++ b/app/src/styles/components/tools.scss @@ -14,6 +14,7 @@ width: fit-content; transition: width 0.2s; background-color: var(--background-color); + z-index: #{$z-index-tools}; .split { height: 20px; width: 2px; @@ -72,6 +73,7 @@ box-shadow: #{$box-shadow-medium}; padding: 8px; border-radius: #{$border-radius-large}; + background: var(--background-color); .option-list { margin: 4px 0; display: flex; diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index b3d98c8..c7cd9e5 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -9,7 +9,7 @@ background-color: var(--background-color); border-radius: #{$border-radius-extra-large}; box-shadow: #{$box-shadow-medium}; - + z-index: #{$z-index-tools}; .header-container { @include flex-space-between; padding: 10px; @@ -179,6 +179,7 @@ background-color: var(--background-color); border-radius: #{$border-radius-extra-large}; box-shadow: #{$box-shadow-medium}; + z-index: #{$z-index-tools}; .header-container { @include flex-space-between; diff --git a/app/src/styles/layout/toast.scss b/app/src/styles/layout/toast.scss index a3f21de..951a213 100644 --- a/app/src/styles/layout/toast.scss +++ b/app/src/styles/layout/toast.scss @@ -1,9 +1,13 @@ +@use "../abstracts/variables" as *; +@use "../abstracts/mixins" as *; + .toast-container { position: fixed; z-index: 1000; display: flex; flex-direction: column; gap: 10px; + z-index: #{$z-index-ui-highest}; } .toast-container.bottom-center { diff --git a/app/src/types/world/worldConstants.ts b/app/src/types/world/worldConstants.ts new file mode 100644 index 0000000..ce5cacd --- /dev/null +++ b/app/src/types/world/worldConstants.ts @@ -0,0 +1,384 @@ +export type Controls = { + azimuthRotateSpeed: number; + polarRotateSpeed: number; + truckSpeed: number; + minDistance: number; + maxDistance: number; + maxPolarAngle: number; + leftMouse: number; + forwardSpeed: number; + backwardSpeed: number; + leftSpeed: number; + rightSpeed: number; +}; + +export type ThirdPersonControls = { + azimuthRotateSpeed: number; + polarRotateSpeed: number; + truckSpeed: number; + maxDistance: number; + maxPolarAngle: number; + minZoom: number; + maxZoom: number; + targetOffset: number; + cameraHeight: number; + leftMouse: number; + rightMouse: number; + wheelMouse: number; + middleMouse: number; +}; + +export type ControlsTransition = { + leftMouse: number; + rightMouse: number; + wheelMouse: number; + middleMouse: number; +}; + +export type TwoDimension = { + defaultPosition: [x: number, y: number, z: number]; + defaultTarget: [x: number, y: number, z: number]; + defaultAzimuth: number; + minDistance: number; + leftMouse: number; + rightMouse: number; +}; + +export type ThreeDimension = { + defaultPosition: [x: number, y: number, z: number]; + defaultTarget: [x: number, y: number, z: number]; + defaultRotation: [x: number, y: number, z: number]; + defaultAzimuth: number; + boundaryBottom: [x: number, y: number, z: number]; + boundaryTop: [x: number, y: number, z: number]; + minDistance: number; + leftMouse: number; + rightMouse: number; +}; + + +export type GridConfig = { + size: number; + divisions: number; + primaryColor: string; + secondaryColor: string; + + position2D: [x: number, y: number, z: number]; + position3D: [x: number, y: number, z: number]; +} + +export type PlaneConfig = { + position2D: [x: number, y: number, z: number]; + position3D: [x: number, y: number, z: number]; + rotation: number; + + width: number; + height: number; + color: string; +} + +export type ShadowConfig = { + shadowOffset: number, + + shadowmapSizewidth: number, + shadowmapSizeheight: number, + shadowcamerafar: number, + shadowcameranear: number, + shadowcameratop: number, + shadowcamerabottom: number, + shadowcameraleft: number, + shadowcameraright: number, + shadowbias: number, + shadownormalBias: number, + + shadowMaterialPosition: [x: number, y: number, z: number], + shadowMaterialRotation: [x: number, y: number, z: number], + + shadowMaterialOpacity: number, +} + +export type SkyConfig = { + defaultTurbidity: number; + maxTurbidity: number; + minTurbidity: number; + defaultRayleigh: number; + mieCoefficient: number; + mieDirectionalG: number; + skyDistance: number; +} + +export type AssetConfig = { + defaultScaleBeforeGsap: [number, number, number]; + defaultScaleAfterGsap: [number, number, number]; +} + +export type PointConfig = { + defaultInnerColor: string; + defaultOuterColor: string; + deleteColor: string; + boxScale: [number, number, number]; + + wallOuterColor: string; + floorOuterColor: string; + aisleOuterColor: string; + zoneOuterColor: string; + + snappingThreshold: number; +} + +export type LineConfig = { + tubularSegments: number; + radius: number; + radialSegments: number; + + wallName: string; + floorName: string; + aisleName: string; + zoneName: string; + referenceName: string; + + lineIntersectionPoints: number; + + defaultColor: string; + + wallColor: string; + floorColor: string; + aisleColor: string; + zoneColor: string; + helperColor: string; +} + +export type WallConfig = { + defaultColor: string; + height: number; + width: number; +} + +export type FloorConfig = { + defaultColor: string; + height: number; + + textureScale: number; +} + +export type RoofConfig = { + defaultColor: string; + height: number; +} + +export type AisleConfig = { + width: number; + height: number; + + defaultColor: number; +} + +export type ZoneConfig = { + defaultColor: string; + + color: string; +} + +export type ColumnConfig = { + defaultColor: string; +} + +export type OutlineConfig = { + assetSelectColor: number; + assetDeleteColor: number; +} + + + + +export const firstPersonControls: Controls = { + azimuthRotateSpeed: 0.3, // Speed of rotation around the azimuth axis + polarRotateSpeed: 0.3, // Speed of rotation around the polar axis + truckSpeed: 10, // Speed of truck movement + minDistance: 0, // Minimum distance from the target + maxDistance: 0, // Maximum distance from the target + maxPolarAngle: Math.PI, // Maximum polar angle + + leftMouse: 1, // Mouse button for rotation (ROTATE) + + forwardSpeed: 0.3, // Speed of forward movement + backwardSpeed: -0.3, // Speed of backward movement + leftSpeed: -0.3, // Speed of left movement + rightSpeed: 0.3, // Speed of right movement +}; + +export const thirdPersonControls: ThirdPersonControls = { + azimuthRotateSpeed: 1, // Speed of rotation around the azimuth axis + polarRotateSpeed: 1, // Speed of rotation around the polar axis + truckSpeed: 2, // Speed of truck movement + maxDistance: 100, // Maximum distance from the target + maxPolarAngle: Math.PI / 2 - 0.05, // Maximum polar angle + minZoom: 6, // Minimum zoom level + maxZoom: 21, // Maximum zoom level + targetOffset: 20, // Offset of the target from the camera + cameraHeight: 30, // Height of the camera + leftMouse: 2, // Mouse button for panning + rightMouse: 1, // Mouse button for rotation + wheelMouse: 8, // Mouse button for zooming + middleMouse: 8, // Mouse button for zooming +}; + +export const controlsTransition: ControlsTransition = { + leftMouse: 0, // Mouse button for no action + rightMouse: 0, // Mouse button for no action + wheelMouse: 0, // Mouse button for no action + middleMouse: 0, // Mouse button for no action +}; + +export const twoDimension: TwoDimension = { + defaultPosition: [0, 100, 0], // Default position of the camera + defaultTarget: [0, 0, 0], // Default target of the camera + defaultAzimuth: 0, // Default azimuth of the camera + minDistance: 25, // Minimum distance from the target + leftMouse: 2, // Mouse button for panning + rightMouse: 0, // Mouse button for no action +}; + +export const threeDimension: ThreeDimension = { + defaultPosition: [0, 40, 30], // Default position of the camera + defaultTarget: [0, 0, 0], // Default target of the camera + defaultRotation: [0, 0, 0], // Default rotation of the camera + defaultAzimuth: 0, // Default azimuth of the camera + boundaryBottom: [-150, 0, -150], // Bottom boundary of the camera movement + boundaryTop: [150, 100, 150], // Top boundary of the camera movement + minDistance: 1, // Minimum distance from the target + leftMouse: 2, // Mouse button for panning + rightMouse: 1, // Mouse button for rotation +}; + +export const camPositionUpdateInterval: number = 200; // Interval for updating the camera position + +export const gridConfig: GridConfig = { + size: 300, // Size of the grid + divisions: 75, // Number of divisions in the grid + primaryColor: "#d5d5d5", // Primary color of the grid + secondaryColor: "#e3e3e3", // Secondary color of the grid + + position2D: [0, 0.1, 0], // Position of the grid in 2D view + position3D: [0, -0.5, 0], // Position of the grid in 3D view +} + +export const planeConfig: PlaneConfig = { + position2D: [0, -0.5, 0], // Position of the plane + position3D: [0, -0.65, 0], // Position of the plane + rotation: -Math.PI / 2, // Rotation of the plane + + width: 300, // Width of the plane + height: 300, // Height of the plane + color: "white" // Color of the plane +} + +export const shadowConfig: ShadowConfig = { + shadowOffset: 50, // Offset of the shadow + + shadowmapSizewidth: 1024, // Width of the shadow map + shadowmapSizeheight: 1024, // Height of the shadow map + // shadowmapSizewidth: 8192, // Width of the shadow map + // shadowmapSizeheight: 8192, // Height of the shadow map + shadowcamerafar: 70, // Far plane of the shadow camera + shadowcameranear: 0.1, // Near plane of the shadow camera + shadowcameratop: 30, // Top plane of the shadow camera + shadowcamerabottom: -30, // Bottom plane of the shadow camera + shadowcameraleft: -30, // Left plane of the shadow camera + shadowcameraright: 30, // Right plane of the shadow camera + shadowbias: -0.001, // Bias of the shadow + shadownormalBias: 0.02, // Normal bias of the shadow + + shadowMaterialPosition: [0, 0.01, 0], // Position of the shadow material + shadowMaterialRotation: [-Math.PI / 2, 0, 0], // Rotation of the shadow material + + shadowMaterialOpacity: 0.1 // Opacity of the shadow material +} + +export const skyConfig: SkyConfig = { + defaultTurbidity: 10.0, // Default turbidity of the sky + maxTurbidity: 20.0, // Maximum turbidity of the sky + minTurbidity: 0.0, // Minimum turbidity of the sky + defaultRayleigh: 1.9, // Default Rayleigh scattering coefficient + mieCoefficient: 0.1, // Mie scattering coefficient + mieDirectionalG: 1.0, // Mie directional G + skyDistance: 2000 // Distance of the sky +} + +export const assetConfig: AssetConfig = { + defaultScaleBeforeGsap: [0.1, 0.1, 0.1], // Default scale of the assets + defaultScaleAfterGsap: [1, 1, 1] // Default scale of the assets +} + +export const pointConfig: PointConfig = { + defaultInnerColor: "#ffffff", // Default inner color of the points + defaultOuterColor: "#ffffff", // Default outer color of the points + deleteColor: "#ff0000", // Color of the points when deleting + boxScale: [0.5, 0.5, 0.5], // Scale of the points + + wallOuterColor: "#C7C7C7", // Outer color of the wall points + floorOuterColor: "#808080", // Outer color of the floor points + aisleOuterColor: "#FBBC05", // Outer color of the aisle points + zoneOuterColor: "#007BFF", // Outer color of the zone points + + snappingThreshold: 1, // Threshold for snapping +} + +export const lineConfig: LineConfig = { + tubularSegments: 64, // Number of tubular segments + radius: 0.15, // Radius of the lines + radialSegments: 8, // Number of radial segments + + wallName: "WallLine", // Name of the wall lines + floorName: "FloorLine", // Name of the floor lines + aisleName: "AisleLine", // Name of the aisle lines + zoneName: "ZoneLine", // Name of the zone lines + referenceName: "ReferenceLine", // Name of the reference lines + + lineIntersectionPoints: 300, // Number of intersection points + + defaultColor: "#000000", // Default color of the lines + + wallColor: "#C7C7C7", // Color of the wall lines + floorColor: "#808080", // Color of the floor lines + aisleColor: "#FBBC05", // Color of the aisle lines + zoneColor: "#007BFF", // Color of the zone lines + helperColor: "#C164FF" // Color of the helper lines +} + +export const wallConfig: WallConfig = { + defaultColor: "white", // Default color of the walls + height: 7, // Height of the walls + width: 0.05, // Width of the walls +} + +export const floorConfig: FloorConfig = { + defaultColor: "grey", // Default color of the floors + height: 0.1, // Height of the floors + textureScale: 0.1, // Scale of the floor texture +} + +export const roofConfig: RoofConfig = { + defaultColor: "grey", // Default color of the roofs + height: 0.1 // Height of the roofs +} + +export const aisleConfig: AisleConfig = { + width: 0.1, // Width of the aisles + height: 0.01, // Height of the aisles + defaultColor: 0xffff00 // Default color of the aisles +} + +export const zoneConfig: ZoneConfig = { + defaultColor: "black", // Default color of the zones + color: "blue" // Color of the zones +} + +export const columnConfig: ColumnConfig = { + defaultColor: "White", // Default color of the columns +} + +export const outlineConfig: OutlineConfig = { + assetSelectColor: 0x0054fE, // Color of the selected assets + assetDeleteColor: 0xFF0000 // Color of the deleted assets +} \ No newline at end of file diff --git a/app/src/types/world/worldTypes.d.ts b/app/src/types/world/worldTypes.d.ts new file mode 100644 index 0000000..9d3fad0 --- /dev/null +++ b/app/src/types/world/worldTypes.d.ts @@ -0,0 +1,307 @@ +// Importing core classes and types from THREE.js and @react-three/fiber +import * as THREE from "three"; +import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; +import { DragControls } from 'three/examples/jsm/controls/DragControls'; +import { IntersectionEvent } from '@react-three/fiber/dist/declarations/src/core/events'; +import { ThreeEvent } from "@react-three/fiber/dist/declarations/src/core/events"; +import { RootState } from "@react-three/fiber"; +import { CSM } from "three/examples/jsm/csm/CSM"; +import { CSMHelper } from 'three/examples/jsm/csm/CSMHelper'; +import { CameraControls } from "@react-three/drei"; + +/** Core THREE.js and React-Fiber Event Types **/ + +// Event type specific to pointer events in @react-three/fiber +export type ThreeEvent = ThreeEvent; + + +/** Vector and Reference Types **/ + +// 2D Vector type from THREE.js +export type Vector2 = THREE.Vector2; + +// React ref for a mutable 2D vector, useful for tracking changes over time +export type RefVector2 = React.MutableRefObject; + +// 3D Vector type from THREE.js +export type Vector3 = THREE.Vector3; + +// Mutable reference for 3D vectors, allowing for dynamic scene changes +export type RefVector3 = React.MutableRefObject; + +// Quaternion type for rotations, using the base structure from THREE.js +export type QuaternionType = THREE.QuaternionLike; + + +/** Basic Object Types for Scene Management **/ + +// THREE.js mesh object +export type Mesh = THREE.Mesh; + +// Color type allowing various formats (hex, RGB, string, etc.) +export type Color = THREE.Color | string | number; + +// Shape type for defining custom geometries +export type Shape = THREE.Shape; + +// Event type for intersections within the scene +export type IntersectionEvent = THREE.Intersection; + +// Array type for intersections with objects in the scene +export type IntersectsType = THREE.Intersection>[]; + +// Event type for mesh interactions +export type MeshEvent = IntersectionEvent; + +// Event type for drag interactions +export type DragEvent = DragEvent; + +// Generic type for user data attached to objects +export type UserData = any; + + +/** React Mutable References for Scene Objects **/ + +// Mutable reference to the scene, used in React-based projects +export type RefScene = React.MutableRefObject; + +// THREE.js group type for grouping objects +export type Group = THREE.Group; + +// Mutable reference for groups, supporting updates in React components +export type RefGroup = React.MutableRefObject; + +// Reference type for meshes, allowing null or undefined values +export type RefMesh = React.MutableRefObject; + +// Mutable reference to camera controls +export type RefControls = React.MutableRefObject; + +// Control types for transformation controls in the scene +export type TransformControl = TransformControl; +export type RefTransformControl = React.MutableRefObject; + +// Array of mesh references for tracking multiple meshes +export type RefMeshArray = React.MutableRefObject; + +// Array of 3D vectors, useful for handling points or positions +export type Vector3Array = THREE.Vector3[]; + +// Drag control type, either a valid DragControls instance or null +export type DragControl = DragControls | null; +export type RefDragControl = React.MutableRefObject; + + +/** Primitive Types with Mutable References **/ + +export type String = string; +export type RefStringArray = React.MutableRefObject; +export type RefString = React.MutableRefObject; + +export type Boolean = boolean; +export type RefBoolean = React.MutableRefObject; + +export type Number = number; +export type NumberArray = number[]; + +// Reference for the THREE.js Raycaster, useful in handling ray intersections +export type RefRaycaster = React.MutableRefObject; + +// Camera reference, supporting both perspective and basic cameras +export type RefCamera = React.MutableRefObject; + + +/** Three.js Root State Management **/ + +// Root state of the @react-three/fiber instance, providing context of the scene +export type ThreeState = RootState; + + +/** Point and Line Types for Spatial Geometry **/ + +// Defines a point in 3D space with metadata for unique identification +export type Point = [THREE.Vector3, string, number, string]; + +// Defines a line as two connected points, commonly used in scene building +export type Line = [Point, Point]; + +// Reference for a line, allowing dynamic updates +export type RefLine = React.MutableRefObject; + +// Collection of lines for structured geometry +export type Lines = Array; + + +/** Wall and Room Types for 3D Space Management **/ + +// Defines a wall with its geometry, position, rotation, material, and layer information +export type Wall = [THREE.ExtrudeGeometry, [number, number, number], [number, number, number], string, number]; + +// Collection of walls, useful in scene construction +export type Walls = Array; + +// Reference type for walls, allowing dynamic updates in React +export type RefWalls = React.MutableRefObject; + +// Room type, containing coordinates and layer metadata for spatial management +export type Rooms = Array<{ coordinates: Array<{ position: THREE.Vector3, uuid: string }>, layer: number }>; + +// Reference for room objects, enabling updates within React components +export type RefRooms = React.MutableRefObject, layer: number }>>; + +// Reference for lines, supporting React-based state changes +export type RefLines = React.MutableRefObject; + + +/** Floor Line Types for Layered Structures **/ + +// Floor line type for single lines on the floor level +export type OnlyFloorLine = Array; + +// Reference for floor lines, allowing manipulation in React +export type RefOnlyFloorLine = React.MutableRefObject; + +// Collection of lines across multiple floors, for multi-level structures +export type OnlyFloorLines = Array; + +// Reference for multi-level floor lines, allowing structured updates +export type RefOnlyFloorLines = React.MutableRefObject; + + +/** GeoJSON Line Integration **/ + +// Structure for representing GeoJSON lines, integrating external data sources +export type GeoJsonLine = { + line: any; + uuids: [string, string]; + layer: number; + type: string; +}; + + +/** State Management Types for React Components **/ + +// Dispatch types for number and boolean states, commonly used in React hooks +export type NumberIncrementState = React.Dispatch>; +export type BooleanState = React.Dispatch>; + +// Mutable reference for TubeGeometry, allowing dynamic geometry updates +export type RefTubeGeometry = React.MutableRefObject; + + +/** Floor Item Configuration **/ + +// Type for individual items placed on the floor, with positioning and rotation metadata +export type FloorItemType = { + modeluuid: string; + modelname: string; + position: [number, number, number]; + rotation: { x: number; y: number; z: number }; + modelfileID?: string; + isLocked: boolean; + isVisible: boolean; +}; + +// Array of floor items for managing multiple objects on the floor +export type FloorItems = Array; + +// Dispatch type for setting floor item state in React +export type setFloorItemSetState = React.Dispatch>; + + +/** Asset Configuration for Loading and Positioning **/ + +// Configuration for assets, allowing model URLs, scaling, positioning, and types +interface AssetConfiguration { + modelUrl: string; + scale?: [number, number, number]; + csgscale?: [number, number, number]; + csgposition?: [number, number, number]; + positionY?: (intersectionPoint: { point: THREE.Vector3 }) => number; + type?: "Fixed-Move" | "Free-Move"; +} + +// Collection of asset configurations, keyed by unique identifiers +export type AssetConfigurations = { + [key: string]: AssetConfiguration; +}; + + +/** Wall Item Configuration **/ + +// Configuration for wall items, including model, scale, position, and rotation +interface WallItem { + type: "Fixed-Move" | "Free-Move" | undefined; + model?: THREE.Group; + modeluuid?: string + modelname?: string; + scale?: [number, number, number]; + csgscale?: [number, number, number]; + csgposition?: [number, number, number]; + position?: [number, number, number]; + quaternion?: Types.QuaternionType; +} + +// Collection of wall items, allowing for multiple items in a scene +export type wallItems = Array; + +// Dispatch for setting wall item state in React +export type setWallItemSetState = React.Dispatch>; + +// Dispatch for setting vector3 state in React +export type setVector3State = React.Dispatch>; + +// Dispatch for setting euler state in React +export type setEulerState = React.Dispatch>; + +// Reference type for wall items, allowing direct access to the mutable array +export type RefWallItems = React.MutableRefObject; + + +/** Wall and Item Selection State Management **/ + +// State management for selecting, removing, and indexing wall items +export type setRemoveLayerSetState = (layer: number | null) => void; +export type setSelectedWallItemSetState = (item: THREE.Object3D | null) => void; +export type setSelectedFloorItemSetState = (item: THREE.Object3D | null) => void; +export type setSelectedItemsIndexSetState = (index: number | null) => void; + +export type RefCSM = React.MutableRefObject; +export type RefCSMHelper = React.MutableRefObject; + + +interface Path { + modeluuid: string; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + events: { uuid: string; type: string; material: string; delay: number | string; spawnInterval: number | string; isUsed: boolean }[] | []; + triggers: { uuid: string; type: string; isUsed: boolean }[] | []; + }[]; + pathPosition: [number, number, number]; + pathRotation: [number, number, number]; + speed: number; +} + +interface SimulationPathsStore { + simulationPaths: Path[]; + setSimulationPaths: (paths: Path[]) => void; +} + +interface PathConnection { + fromPathUUID: string; + fromUUID: string; + toConnections: { + toPathUUID: string; + toUUID: string; + }[]; +} + +interface ConnectionStore { + connections: PathConnection[]; + setConnections: (connections: PathConnection[]) => void; + addConnection: (newConnection: PathConnection) => void; + removeConnection: (fromUUID: string, toUUID: string) => void; +} \ No newline at end of file diff --git a/app/src/functions/idbUtils.ts b/app/src/utils/indexDB/idbUtils.ts similarity index 100% rename from app/src/functions/idbUtils.ts rename to app/src/utils/indexDB/idbUtils.ts