diff --git a/app/.env b/app/.env index e69de29..4d1f42c 100644 --- a/app/.env +++ b/app/.env @@ -0,0 +1,11 @@ +# Port on which the Docsify documentation server will run. +DOCSIFY_PORT=8201 + +# Base URL for the server socket API, used for real-time communication (e.g., WebSockets). +REACT_APP_SERVER_SOCKET_API_BASE_URL=185.100.212.76:8000 + +# Base URL for the server REST API, used for HTTP requests to the backend server. +REACT_APP_SERVER_REST_API_BASE_URL=185.100.212.76:5000 + +# Base URL for the server marketplace API. +REACT_APP_SERVER_MARKETPLACE_URL=185.100.212.76:50011 diff --git a/app/package-lock.json b/app/package-lock.json index 937d3e6..928b583 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,18 +8,34 @@ "name": "react", "version": "0.0.0", "dependencies": { + "@react-three/csg": "^4.0.0", + "@react-three/drei": "^10.0.4", + "@react-three/fiber": "^9.1.0", + "@react-three/postprocessing": "^3.0.4", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", + "@turf/turf": "^7.2.0", + "@types/jest": "^29.5.14", + "@use-gesture/react": "^10.3.1", "chart.js": "^4.4.8", + "gsap": "^3.12.7", + "leva": "^0.10.0", + "mqtt": "^5.10.4", "path": "^0.12.7", + "postprocessing": "^6.37.1", "react": "^19.0.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.3.0", + "react-toastify": "^11.0.5", + "socket.io-client": "^4.8.1", "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.21.0", "@types/node": "^22.13.10", - "@types/react": "^19.0.10", + "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.21.0", @@ -33,6 +49,12 @@ "vite": "^6.2.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", + "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -51,7 +73,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -193,7 +214,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -271,6 +291,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", @@ -912,6 +944,35 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.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/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -978,6 +1039,47 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", @@ -1037,6 +1139,24 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.1.0.tgz", + "integrity": "sha512-Obb0/gEd/HReTlg8ttaYk+0m62gQJmCblMOjHSMHRrBP2zdfKMHLCRbh/6ex9fSUJMKdjjIEiohwkbGD3wj2Nw==", + "license": "MIT", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1385,6 +1505,422 @@ "url": "https://opencollective.com/parcel" } }, + "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-three/csg": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-three/csg/-/csg-4.0.0.tgz", + "integrity": "sha512-IMLKGINdyQEUfyg3CE1gYoH9lK5RDR90vhXG21vHyo2kVAuslTnsH9GTVat69YglvNqHoEIMExTGLdhzm5/ygQ==", + "license": "MIT", + "dependencies": { + "three-bvh-csg": "^0.0.16", + "three-mesh-bvh": "^0.6.8" + } + }, + "node_modules/@react-three/drei": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.0.4.tgz", + "integrity": "sha512-/ZtU4DAkJg72ipsa/UHXJ6SFs45G/rTzV+TdgZH2vyqaNbnFqNHQNXpr/HXWtceZOYI8Gzlv1yPAuk8EjuhLSA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mediapipe/tasks-vision": "0.10.17", + "@monogrid/gainmap-js": "^3.0.6", + "@use-gesture/react": "^10.3.1", + "camera-controls": "^2.9.0", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.56", + "glsl-noise": "^0.0.0", + "hls.js": "^1.5.17", + "maath": "^0.10.8", + "meshline": "^3.3.1", + "stats-gl": "^2.2.8", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.8.3", + "three-stdlib": "^2.35.6", + "troika-three-text": "^0.52.0", + "tunnel-rat": "^0.1.2", + "use-sync-external-store": "^1.4.0", + "utility-types": "^3.11.0", + "zustand": "^5.0.1" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19", + "react-dom": "^19", + "three": ">=0.159" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/drei/node_modules/three-mesh-bvh": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz", + "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.159.0" + } + }, + "node_modules/@react-three/fiber": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.1.0.tgz", + "integrity": "sha512-r/a0dpqdz5ci17yMIWE+70WwxiTScGFEyvtDj0o4isZ7YUvPu0k78Zl7cJGL+KhheKXCzbNNxEz4+lFan6atyg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.17.8", + "@types/react-reconciler": "^0.28.9", + "@types/webxr": "*", + "base64-js": "^1.5.1", + "buffer": "^6.0.3", + "its-fine": "^2.0.0", + "react-reconciler": "^0.31.0", + "react-use-measure": "^2.1.7", + "scheduler": "^0.25.0", + "suspend-react": "^0.1.3", + "use-sync-external-store": "^1.4.0", + "zustand": "^5.0.3" + }, + "peerDependencies": { + "expo": ">=43.0", + "expo-asset": ">=8.4", + "expo-file-system": ">=11.0", + "expo-gl": ">=11.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-native": ">=0.78", + "three": ">=0.156" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + }, + "expo-asset": { + "optional": true + }, + "expo-file-system": { + "optional": true + }, + "expo-gl": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@react-three/postprocessing": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@react-three/postprocessing/-/postprocessing-3.0.4.tgz", + "integrity": "sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==", + "license": "MIT", + "dependencies": { + "maath": "^0.6.0", + "n8ao": "^1.9.4", + "postprocessing": "^6.36.6" + }, + "peerDependencies": { + "@react-three/fiber": "^9.0.0", + "react": "^19.0", + "three": ">= 0.156.0" + } + }, + "node_modules/@react-three/postprocessing/node_modules/maath": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.6.0.tgz", + "integrity": "sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.35.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.35.0.tgz", @@ -1651,6 +2187,2133 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "license": "MIT" + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "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/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz", + "integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@turf/along": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/along/-/along-7.2.0.tgz", + "integrity": "sha512-Cf+d2LozABdb0TJoIcJwFKB+qisJY4nMUW9z6PAuZ9UCH7AR//hy2Z06vwYCKFZKP4a7DRPkOMBadQABCyoYuw==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^7.2.0", + "@turf/destination": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/angle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/angle/-/angle-7.2.0.tgz", + "integrity": "sha512-b28rs1NO8Dt/MXadFhnpqH7GnEWRsl+xF5JeFtg9+eM/+l/zGrdliPYMZtAj12xn33w22J1X4TRprAI0rruvVQ==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/rhumb-bearing": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/area": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.2.0.tgz", + "integrity": "sha512-zuTTdQ4eoTI9nSSjerIy4QwgvxqwJVciQJ8tOPuMHbXJ9N/dNjI7bU8tasjhxas/Cx3NE9NxVHtNpYHL0FSzoA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.2.0.tgz", + "integrity": "sha512-wzHEjCXlYZiDludDbXkpBSmv8Zu6tPGLmJ1sXQ6qDwpLE1Ew3mcWqt8AaxfTP5QwDNQa3sf2vvgTEzNbPQkCiA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox-clip": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/bbox-clip/-/bbox-clip-7.2.0.tgz", + "integrity": "sha512-q6RXTpqeUQAYLAieUL1n3J6ukRGsNVDOqcYtfzaJbPW+0VsAf+1cI16sN700t0sekbeU1DH/RRVAHhpf8+36wA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox-polygon": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/bbox-polygon/-/bbox-polygon-7.2.0.tgz", + "integrity": "sha512-Aj4G1GAAy26fmOqMjUk0Z+Lcax5VQ9g1xYDbHLQWXvfTsaueBT+RzdH6XPnZ/seEEnZkio2IxE8V5af/osupgA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bearing": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/bearing/-/bearing-7.2.0.tgz", + "integrity": "sha512-Jm0Xt3GgHjRrWvBtAGvgfnADLm+4exud2pRlmCYx8zfiKuNXQFkrcTZcOiJOgTfG20Agq28iSh15uta47jSIbg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bezier-spline": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/bezier-spline/-/bezier-spline-7.2.0.tgz", + "integrity": "sha512-7BPkc3ufYB9KLvcaTpTsnpXzh9DZoENxCS0Ms9XUwuRXw45TpevwUpOsa3atO76iKQ5puHntqFO4zs8IUxBaaA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-clockwise": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-7.2.0.tgz", + "integrity": "sha512-0fJeFSARxy6ealGBM4Gmgpa1o8msQF87p2Dx5V6uSqzT8VPDegX1NSWl4b7QgXczYa9qv7IAABttdWP0K7Q7eQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-concave": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-concave/-/boolean-concave-7.2.0.tgz", + "integrity": "sha512-v3dTN04dfO6VqctQj1a+pjDHb6+/Ev90oAR2QjJuAntY4ubhhr7vKeJdk/w+tWNSMKULnYwfe65Du3EOu3/TeA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-contains": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-7.2.0.tgz", + "integrity": "sha512-dgRQm4uVO5XuLee4PLVH7CFQZKdefUBMIXTPITm2oRIDmPLJKHDOFKQTNkGJ73mDKKBR2lmt6eVH3br6OYrEYg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/boolean-point-on-line": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-crosses": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-crosses/-/boolean-crosses-7.2.0.tgz", + "integrity": "sha512-9GyM4UUWFKQOoNhHVSfJBf5XbPy8Fxfz9djjJNAnm/IOl8NmFUSwFPAjKlpiMcr6yuaAoc9R/1KokS9/eLqPvA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/line-intersect": "^7.2.0", + "@turf/polygon-to-line": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-disjoint": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-disjoint/-/boolean-disjoint-7.2.0.tgz", + "integrity": "sha512-xdz+pYKkLMuqkNeJ6EF/3OdAiJdiHhcHCV0ykX33NIuALKIEpKik0+NdxxNsZsivOW6keKwr61SI+gcVtHYcnQ==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/line-intersect": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/polygon-to-line": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-equal": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-equal/-/boolean-equal-7.2.0.tgz", + "integrity": "sha512-TmjKYLsxXqEmdDtFq3QgX4aSogiISp3/doeEtDOs3NNSR8susOtBEZkmvwO6DLW+g/rgoQJIBR6iVoWiRqkBxw==", + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "geojson-equality-ts": "^1.0.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-intersects": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-intersects/-/boolean-intersects-7.2.0.tgz", + "integrity": "sha512-GLRyLQgK3F14drkK5Qi9Mv7Z9VT1bgQUd9a3DB3DACTZWDSwfh8YZUFn/HBwRkK8dDdgNEXaavggQHcPi1k9ow==", + "license": "MIT", + "dependencies": { + "@turf/boolean-disjoint": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-overlap": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-overlap/-/boolean-overlap-7.2.0.tgz", + "integrity": "sha512-ieM5qIE4anO+gUHIOvEN7CjyowF+kQ6v20/oNYJCp63TVS6eGMkwgd+I4uMzBXfVW66nVHIXjODdUelU+Xyctw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/line-intersect": "^7.2.0", + "@turf/line-overlap": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "geojson-equality-ts": "^1.0.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-parallel": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-parallel/-/boolean-parallel-7.2.0.tgz", + "integrity": "sha512-iOtuzzff8nmwv05ROkSvyeGLMrfdGkIi+3hyQ+DH4IVyV37vQbqR5oOJ0Nt3Qq1Tjrq9fvF8G3OMdAv3W2kY9w==", + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/line-segment": "^7.2.0", + "@turf/rhumb-bearing": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.2.0.tgz", + "integrity": "sha512-lvEOjxeXIp+wPXgl9kJA97dqzMfNexjqHou+XHVcfxQgolctoJiRYmcVCWGpiZ9CBf/CJha1KmD1qQoRIsjLaA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "point-in-polygon-hao": "^1.1.0", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-on-line": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-7.2.0.tgz", + "integrity": "sha512-H/bXX8+2VYeSyH8JWrOsu8OGmeA9KVZfM7M6U5/fSqGsRHXo9MyYJ94k39A9kcKSwI0aWiMXVD2UFmiWy8423Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-touches": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-touches/-/boolean-touches-7.2.0.tgz", + "integrity": "sha512-8qb1CO+cwFATGRGFgTRjzL9aibfsbI91pdiRl7KIEkVdeN/H9k8FDrUA1neY7Yq48IaciuwqjbbojQ16FD9b0w==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/boolean-point-on-line": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-valid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-valid/-/boolean-valid-7.2.0.tgz", + "integrity": "sha512-xb7gdHN8VV6ivPJh6rPpgxmAEGReiRxqY+QZoEZVGpW2dXcmU1BdY6FA6G/cwvggXAXxJBREoANtEDgp/0ySbA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/boolean-crosses": "^7.2.0", + "@turf/boolean-disjoint": "^7.2.0", + "@turf/boolean-overlap": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/boolean-point-on-line": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/line-intersect": "^7.2.0", + "@types/geojson": "^7946.0.10", + "geojson-polygon-self-intersections": "^1.2.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-within": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-within/-/boolean-within-7.2.0.tgz", + "integrity": "sha512-zB3AiF59zQZ27Dp1iyhp9mVAKOFHat8RDH45TZhLY8EaqdEPdmLGvwMFCKfLryQcUDQvmzP8xWbtUR82QM5C4g==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/boolean-point-on-line": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/buffer": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/buffer/-/buffer-7.2.0.tgz", + "integrity": "sha512-QH1FTr5Mk4z1kpQNztMD8XBOZfpOXPOtlsxaSAj2kDIf5+LquA6HtJjZrjUngnGtzG5+XwcfyRL4ImvLnFjm5Q==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/center": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/jsts": "^2.7.1", + "@turf/meta": "^7.2.0", + "@turf/projection": "^7.2.0", + "@types/geojson": "^7946.0.10", + "d3-geo": "1.7.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/center/-/center-7.2.0.tgz", + "integrity": "sha512-UTNp9abQ2kuyRg5gCIGDNwwEQeF3NbpYsd1Q0KW9lwWuzbLVNn0sOwbxjpNF4J2HtMOs5YVOcqNvYyuoa2XrXw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center-mean": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/center-mean/-/center-mean-7.2.0.tgz", + "integrity": "sha512-NaW6IowAooTJ35O198Jw3U4diZ6UZCCeJY+4E+WMLpks3FCxMDSHEfO2QjyOXQMGWZnVxVelqI5x9DdniDbQ+A==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center-median": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/center-median/-/center-median-7.2.0.tgz", + "integrity": "sha512-/CgVyHNG4zAoZpvkl7qBCe4w7giWNVtLyTU5PoIfg1vWM4VpYw+N7kcBBH46bbzvVBn0vhmZr586r543EwdC/A==", + "license": "MIT", + "dependencies": { + "@turf/center-mean": "^7.2.0", + "@turf/centroid": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center-of-mass": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/center-of-mass/-/center-of-mass-7.2.0.tgz", + "integrity": "sha512-ij3pmG61WQPHGTQvOziPOdIgwTMegkYTwIc71Gl7xn4C0vWH6KLDSshCphds9xdWSXt2GbHpUs3tr4XGntHkEQ==", + "license": "MIT", + "dependencies": { + "@turf/centroid": "^7.2.0", + "@turf/convex": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/centroid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-7.2.0.tgz", + "integrity": "sha512-yJqDSw25T7P48au5KjvYqbDVZ7qVnipziVfZ9aSo7P2/jTE7d4BP21w0/XLi3T/9bry/t9PR1GDDDQljN4KfDw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/circle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/circle/-/circle-7.2.0.tgz", + "integrity": "sha512-1AbqBYtXhstrHmnW6jhLwsv7TtmT0mW58Hvl1uZXEDM1NCVXIR50yDipIeQPjrCuJ/Zdg/91gU8+4GuDCAxBGA==", + "license": "MIT", + "dependencies": { + "@turf/destination": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clean-coords": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-7.2.0.tgz", + "integrity": "sha512-+5+J1+D7wW7O/RDXn46IfCHuX1gIV1pIAQNSA7lcDbr3HQITZj334C4mOGZLEcGbsiXtlHWZiBtm785Vg8i+QQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clone": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-7.2.0.tgz", + "integrity": "sha512-JlGUT+/5qoU5jqZmf6NMFIoLDY3O7jKd53Up+zbpJ2vzUp6QdwdNzwrsCeONhynWM13F0MVtPXH4AtdkrgFk4g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clusters": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/clusters/-/clusters-7.2.0.tgz", + "integrity": "sha512-sKOrIKHHtXAuTKNm2USnEct+6/MrgyzMW42deZ2YG2RRKWGaaxHMFU2Yw71Yk4DqStOqTIBQpIOdrRuSOwbuQw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clusters-dbscan": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/clusters-dbscan/-/clusters-dbscan-7.2.0.tgz", + "integrity": "sha512-VWVUuDreev56g3/BMlnq/81yzczqaz+NVTypN5CigGgP67e+u/CnijphiuhKjtjDd/MzGjXgEWBJc26Y6LYKAw==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clusters-kmeans": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/clusters-kmeans/-/clusters-kmeans-7.2.0.tgz", + "integrity": "sha512-BxQdK8jc8Mwm9yoClCYkktm4W004uiQGqb/i/6Y7a8xqgJITWDgTu/cy//wOxAWPk4xfe6MThjnqkszWW8JdyQ==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "skmeans": "0.9.7", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/collect": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/collect/-/collect-7.2.0.tgz", + "integrity": "sha512-zRVGDlYS8Bx/Zz4vnEUyRg4dmqHhkDbW/nIUIJh657YqaMj1SFi4Iv2i9NbcurlUBDJFkpuOhCvvEvAdskJ8UA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/combine": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/combine/-/combine-7.2.0.tgz", + "integrity": "sha512-VEjm3IvnbMt3IgeRIhCDhhQDbLqCU1/5uN1+j1u6fyA095pCizPThGp4f/COSzC3t1s/iiV+fHuDsB6DihHffQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/concave": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/concave/-/concave-7.2.0.tgz", + "integrity": "sha512-cpaDDlumK762kdadexw5ZAB6g/h2pJdihZ+e65lbQVe3WukJHAANnIEeKsdFCuIyNKrwTz2gWu5ws+OpjP48Yw==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/tin": "^7.2.0", + "@types/geojson": "^7946.0.10", + "topojson-client": "3.x", + "topojson-server": "3.x", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/convex": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/convex/-/convex-7.2.0.tgz", + "integrity": "sha512-HsgHm+zHRE8yPCE/jBUtWFyaaBmpXcSlyHd5/xsMhSZRImFzRzBibaONWQo7xbKZMISC3Nc6BtUjDi/jEVbqyA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "concaveman": "^1.2.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/destination": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/destination/-/destination-7.2.0.tgz", + "integrity": "sha512-8DUxtOO0Fvrh1xclIUj3d9C5WS20D21F5E+j+X9Q+ju6fcM4huOqTg5ckV1DN2Pg8caABEc5HEZJnGch/5YnYQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/difference": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/difference/-/difference-7.2.0.tgz", + "integrity": "sha512-NHKD1v3s8RX+9lOpvHJg6xRuJOKiY3qxHhz5/FmE0VgGqnCkE7OObqWZ5SsXG+Ckh0aafs5qKhmDdDV/gGi6JA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/dissolve": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/dissolve/-/dissolve-7.2.0.tgz", + "integrity": "sha512-gPG5TE3mAYuZqBut8tPYCKwi4hhx5Cq0ALoQMB9X0hrVtFIKrihrsj98XQM/5pL/UIpAxQfwisQvy6XaOFaoPA==", + "license": "MIT", + "dependencies": { + "@turf/flatten": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/distance": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-7.2.0.tgz", + "integrity": "sha512-HBjjXIgEcD/wJYjv7/6OZj5yoky2oUvTtVeIAqO3lL80XRvoYmVg6vkOIu6NswkerwLDDNT9kl7+BFLJoHbh6Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/distance-weight": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/distance-weight/-/distance-weight-7.2.0.tgz", + "integrity": "sha512-NeoyV0fXDH+7nIoNtLjAoH9XL0AS1pmTIyDxEE6LryoDTsqjnuR0YQxIkLCCWDqECoqaOmmBqpeWONjX5BwWCg==", + "license": "MIT", + "dependencies": { + "@turf/centroid": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/ellipse": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/ellipse/-/ellipse-7.2.0.tgz", + "integrity": "sha512-/Y75S5hE2+xjnTw4dXpQ5r/Y2HPM4xrwkPRCCQRpuuboKdEvm42azYmh7isPnMnBTVcmGb9UmGKj0HHAbiwt1g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/rhumb-destination": "^7.2.0", + "@turf/transform-rotate": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/envelope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/envelope/-/envelope-7.2.0.tgz", + "integrity": "sha512-xOMtDeNKHwUuDfzQeoSNmdabsP0/IgVDeyzitDe/8j9wTeW+MrKzVbGz7627PT3h6gsO+2nUv5asfKtUbmTyHA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/bbox-polygon": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/explode": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/explode/-/explode-7.2.0.tgz", + "integrity": "sha512-jyMXg93J1OI7/65SsLE1k9dfQD3JbcPNMi4/O3QR2Qb3BAs2039oFaSjtW+YqhMqVC4V3ZeKebMcJ8h9sK1n+A==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/flatten": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/flatten/-/flatten-7.2.0.tgz", + "integrity": "sha512-q38Qsqr4l7mxp780zSdn0gp/WLBX+sa+gV6qIbDQ1HKCrrPK8QQJmNx7gk1xxEXVot6tq/WyAPysCQdX+kLmMA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/flip": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/flip/-/flip-7.2.0.tgz", + "integrity": "sha512-X0TQ0U/UYh4tyXdLO5itP1sO2HOvfrZC0fYSWmTfLDM14jEPkEK8PblofznfBygL+pIFtOS2is8FuVcp5XxYpQ==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/geojson-rbush": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/geojson-rbush/-/geojson-rbush-7.2.0.tgz", + "integrity": "sha512-ST8fLv+EwxVkDgsmhHggM0sPk2SfOHTZJkdgMXVFT7gB9o4lF8qk4y4lwvCCGIfFQAp2yv/PN5EaGMEKutk6xw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/great-circle": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/great-circle/-/great-circle-7.2.0.tgz", + "integrity": "sha512-n30OiADyOKHhor0aXNgYfXQYXO3UtsOKmhQsY1D89/Oh1nCIXG/1ZPlLL9ZoaRXXBTUBjh99a+K8029NQbGDhw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.2.0.tgz", + "integrity": "sha512-cXo7bKNZoa7aC7ydLmUR02oB3IgDe7MxiPuRz3cCtYQHn+BJ6h1tihmamYDWWUlPHgSNF0i3ATc4WmDECZafKw==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/hex-grid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/hex-grid/-/hex-grid-7.2.0.tgz", + "integrity": "sha512-Yo2yUGxrTCQfmcVsSjDt0G3Veg8YD26WRd7etVPD9eirNNgXrIyZkbYA7zVV/qLeRWVmYIKRXg1USWl7ORQOGA==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/intersect": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/interpolate": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/interpolate/-/interpolate-7.2.0.tgz", + "integrity": "sha512-Ifgjm1SEo6XujuSAU6lpRMvoJ1SYTreil1Rf5WsaXj16BQJCedht/4FtWCTNhSWTwEz2motQ1WNrjTCuPG94xA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/centroid": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/hex-grid": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/point-grid": "^7.2.0", + "@turf/square-grid": "^7.2.0", + "@turf/triangle-grid": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/intersect": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/intersect/-/intersect-7.2.0.tgz", + "integrity": "sha512-81GMzKS9pKqLPa61qSlFxLFeAC8XbwyCQ9Qv4z6o5skWk1qmMUbEHeMqaGUTEzk+q2XyhZ0sju1FV4iLevQ/aw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.2.0.tgz", + "integrity": "sha512-kV4u8e7Gkpq+kPbAKNC21CmyrXzlbBgFjO1PhrHPgEdNqXqDawoZ3i6ivE3ULJj2rSesCjduUaC/wyvH/sNr2Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/isobands": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/isobands/-/isobands-7.2.0.tgz", + "integrity": "sha512-lYoHeRieFzpBp29Jh19QcDIb0E+dzo/K5uwZuNga4wxr6heNU0AfkD4ByAHYIXHtvmp4m/JpSKq/2N6h/zvBkg==", + "license": "MIT", + "dependencies": { + "@turf/area": "^7.2.0", + "@turf/bbox": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/explode": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "marchingsquares": "^1.3.3", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/isolines": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/isolines/-/isolines-7.2.0.tgz", + "integrity": "sha512-4ZXKxvA/JKkxAXixXhN3UVza5FABsdYgOWXyYm3L5ryTPJVOYTVSSd9A+CAVlv9dZc3YdlsqMqLTXNOOre/kwg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "marchingsquares": "^1.3.3", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/jsts": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@turf/jsts/-/jsts-2.7.2.tgz", + "integrity": "sha512-zAezGlwWHPyU0zxwcX2wQY3RkRpwuoBmhhNE9HY9kWhFDkCxZ3aWK5URKwa/SWKJbj9aztO+8vtdiBA28KVJFg==", + "license": "(EDL-1.0 OR EPL-1.0)", + "dependencies": { + "jsts": "2.7.1" + } + }, + "node_modules/@turf/kinks": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/kinks/-/kinks-7.2.0.tgz", + "integrity": "sha512-BtxDxGewJR0Q5WR9HKBSxZhirFX+GEH1rD7/EvgDsHS8e1Y5/vNQQUmXdURjdPa4StzaUBsWRU5T3A356gLbPA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/length": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/length/-/length-7.2.0.tgz", + "integrity": "sha512-LBmYN+iCgVtWNLsckVnpQIJENqIIPO63mogazMp23lrDGfWXu07zZQ9ZinJVO5xYurXNhc/QI2xxoqt2Xw90Ig==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-arc": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-arc/-/line-arc-7.2.0.tgz", + "integrity": "sha512-kfWzA5oYrTpslTg5fN50G04zSypiYQzjZv3FLjbZkk6kta5fo4JkERKjTeA8x4XNojb+pfmjMBB0yIh2w2dDRw==", + "license": "MIT", + "dependencies": { + "@turf/circle": "^7.2.0", + "@turf/destination": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-chunk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-chunk/-/line-chunk-7.2.0.tgz", + "integrity": "sha512-1ODyL5gETtWSL85MPI0lgp/78vl95M39gpeBxePXyDIqx8geDP9kXfAzctuKdxBoR4JmOVM3NT7Fz7h+IEkC+g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/length": "^7.2.0", + "@turf/line-slice-along": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-intersect": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-7.2.0.tgz", + "integrity": "sha512-GhCJVEkc8EmggNi85EuVLoXF5T5jNVxmhIetwppiVyJzMrwkYAkZSYB3IBFYGUUB9qiNFnTwungVSsBV/S8ZiA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "sweepline-intersections": "^1.5.0", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-offset": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-offset/-/line-offset-7.2.0.tgz", + "integrity": "sha512-1+OkYueDCbnEWzbfBh3taVr+3SyM2bal5jfnSEuDiLA6jnlScgr8tn3INo+zwrUkPFZPPAejL1swVyO5TjUahw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-overlap": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-overlap/-/line-overlap-7.2.0.tgz", + "integrity": "sha512-NNn7/jg53+N10q2Kyt66bEDqN3101iW/1zA5FW7J6UbKApDFkByh+18YZq1of71kS6oUYplP86WkDp16LFpqqw==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-on-line": "^7.2.0", + "@turf/geojson-rbush": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/line-segment": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/nearest-point-on-line": "^7.2.0", + "@types/geojson": "^7946.0.10", + "fast-deep-equal": "^3.1.3", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-segment": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-7.2.0.tgz", + "integrity": "sha512-E162rmTF9XjVN4rINJCd15AdQGCBlNqeWN3V0YI1vOUpZFNT2ii4SqEMCcH2d+5EheHLL8BWVwZoOsvHZbvaWA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-slice": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-slice/-/line-slice-7.2.0.tgz", + "integrity": "sha512-bHotzZIaU1GPV3RMwttYpDrmcvb3X2i1g/WUttPZWtKrEo2VVAkoYdeZ2aFwtogERYS4quFdJ/TDzAtquBC8WQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/nearest-point-on-line": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-slice-along": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-slice-along/-/line-slice-along-7.2.0.tgz", + "integrity": "sha512-4/gPgP0j5Rp+1prbhXqn7kIH/uZTmSgiubUnn67F8nb9zE+MhbRglhSlRYEZxAVkB7VrGwjyolCwvrROhjHp2A==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^7.2.0", + "@turf/destination": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-split": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-split/-/line-split-7.2.0.tgz", + "integrity": "sha512-yJTZR+c8CwoKqdW/aIs+iLbuFwAa3Yan+EOADFQuXXIUGps3bJUXx/38rmowNoZbHyP1np1+OtrotyHu5uBsfQ==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/geojson-rbush": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/line-intersect": "^7.2.0", + "@turf/line-segment": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/nearest-point-on-line": "^7.2.0", + "@turf/square": "^7.2.0", + "@turf/truncate": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-to-polygon": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/line-to-polygon/-/line-to-polygon-7.2.0.tgz", + "integrity": "sha512-iKpJqc7EYc5NvlD4KaqrKKO6mXR7YWO/YwtW60E2FnsF/blnsy9OfAOcilYHgH3S/V/TT0VedC7DW7Kgjy2EIA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/mask": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/mask/-/mask-7.2.0.tgz", + "integrity": "sha512-ulJ6dQqXC0wrjIoqFViXuMUdIPX5Q6GPViZ3kGfeVijvlLM7kTFBsZiPQwALSr5nTQg4Ppf3FD0Jmg8IErPrgA==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.2.0.tgz", + "integrity": "sha512-igzTdHsQc8TV1RhPuOLVo74Px/hyPrVgVOTgjWQZzt3J9BVseCdpfY/0cJBdlSRI4S/yTmmHl7gAqjhpYH5Yaw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/midpoint": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/midpoint/-/midpoint-7.2.0.tgz", + "integrity": "sha512-AMn5S9aSrbXdE+Q4Rj+T5nLdpfpn+mfzqIaEKkYI021HC0vb22HyhQHsQbSeX+AWcS4CjD1hFsYVcgKI+5qCfw==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^7.2.0", + "@turf/destination": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/moran-index": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/moran-index/-/moran-index-7.2.0.tgz", + "integrity": "sha512-Aexh1EmXVPJhApr9grrd120vbalIthcIsQ3OAN2Tqwf+eExHXArJEJqGBo9IZiQbIpFJeftt/OvUvlI8BeO1bA==", + "license": "MIT", + "dependencies": { + "@turf/distance-weight": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-neighbor-analysis": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-neighbor-analysis/-/nearest-neighbor-analysis-7.2.0.tgz", + "integrity": "sha512-LmP/crXb7gilgsL0wL9hsygqc537W/a1W5r9XBKJT4SKdqjoXX5APJatJfd3nwXbRIqwDH0cDA9/YyFjBPlKnA==", + "license": "MIT", + "dependencies": { + "@turf/area": "^7.2.0", + "@turf/bbox": "^7.2.0", + "@turf/bbox-polygon": "^7.2.0", + "@turf/centroid": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/nearest-point": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point/-/nearest-point-7.2.0.tgz", + "integrity": "sha512-0wmsqXZ8CGw4QKeZmS+NdjYTqCMC+HXZvM3XAQIU6k6laNLqjad2oS4nDrtcRs/nWDvcj1CR+Io7OiQ6sbpn5Q==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-on-line": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-7.2.0.tgz", + "integrity": "sha512-UOhAeoDPVewBQV+PWg1YTMQcYpJsIqfW5+EuZ5vJl60XwUa0+kqB/eVfSLNXmHENjKKIlEt9Oy9HIDF4VeWmXA==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-to-line": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-to-line/-/nearest-point-to-line-7.2.0.tgz", + "integrity": "sha512-EorU7Qj30A7nAjh++KF/eTPDlzwuuV4neBz7tmSTB21HKuXZAR0upJsx6M2X1CSyGEgNsbFB0ivNKIvymRTKBw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/point-to-line-distance": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/planepoint": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/planepoint/-/planepoint-7.2.0.tgz", + "integrity": "sha512-8Vno01tvi5gThUEKBQ46CmlEKDAwVpkl7stOPFvJYlA1oywjAL4PsmgwjXgleZuFtXQUPBNgv5a42Pf438XP4g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-grid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/point-grid/-/point-grid-7.2.0.tgz", + "integrity": "sha512-ai7lwBV2FREPW3XiUNohT4opC1hd6+F56qZe20xYhCTkTD9diWjXHiNudQPSmVAUjgMzQGasblQQqvOdL+bJ3Q==", + "license": "MIT", + "dependencies": { + "@turf/boolean-within": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-on-feature": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/point-on-feature/-/point-on-feature-7.2.0.tgz", + "integrity": "sha512-ksoYoLO9WtJ/qI8VI9ltF+2ZjLWrAjZNsCsu8F7nyGeCh4I8opjf4qVLytFG44XA2qI5yc6iXDpyv0sshvP82Q==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/center": "^7.2.0", + "@turf/explode": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/nearest-point": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-to-line-distance": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/point-to-line-distance/-/point-to-line-distance-7.2.0.tgz", + "integrity": "sha512-fB9Rdnb5w5+t76Gho2dYDkGe20eRrFk8CXi4v1+l1PC8YyLXO+x+l3TrtT8HzL/dVaZeepO6WUIsIw3ditTOPg==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/nearest-point-on-line": "^7.2.0", + "@turf/projection": "^7.2.0", + "@turf/rhumb-bearing": "^7.2.0", + "@turf/rhumb-distance": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-to-polygon-distance": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/point-to-polygon-distance/-/point-to-polygon-distance-7.2.0.tgz", + "integrity": "sha512-w+WYuINgTiFjoZemQwOaQSje/8Kq+uqJOynvx7+gleQPHyWQ3VtTodtV4LwzVzXz8Sf7Mngx1Jcp2SNai5CJYA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/point-to-line-distance": "^7.2.0", + "@turf/polygon-to-line": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/points-within-polygon": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/points-within-polygon/-/points-within-polygon-7.2.0.tgz", + "integrity": "sha512-jRKp8/mWNMzA+hKlQhxci97H5nOio9tp14R2SzpvkOt+cswxl+NqTEi1hDd2XetA7tjU0TSoNjEgVY8FfA0S6w==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-smooth": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-smooth/-/polygon-smooth-7.2.0.tgz", + "integrity": "sha512-KCp9wF2IEynvGXVhySR8oQ2razKP0zwg99K+fuClP21pSKCFjAPaihPEYq6e8uI/1J7ibjL5++6EMl+LrUTrLg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-tangents": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-tangents/-/polygon-tangents-7.2.0.tgz", + "integrity": "sha512-AHUUPmOjiQDrtP/ODXukHBlUG0C/9I1je7zz50OTfl2ZDOdEqFJQC3RyNELwq07grTXZvg5TS5wYx/Y7nsm47g==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/boolean-within": "^7.2.0", + "@turf/explode": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/nearest-point": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-to-line": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-to-line/-/polygon-to-line-7.2.0.tgz", + "integrity": "sha512-9jeTN3LiJ933I5sd4K0kwkcivOYXXm1emk0dHorwXeSFSHF+nlYesEW3Hd889wb9lZd7/SVLMUeX/h39mX+vCA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygonize": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/polygonize/-/polygonize-7.2.0.tgz", + "integrity": "sha512-U9v+lBhUPDv+nsg/VcScdiqCB59afO6CHDGrwIl2+5i6Ve+/KQKjpTV/R+NqoC1iMXAEq3brY6HY8Ukp/pUWng==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/envelope": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/projection": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/projection/-/projection-7.2.0.tgz", + "integrity": "sha512-/qke5vJScv8Mu7a+fU3RSChBRijE6EVuFHU3RYihMuYm04Vw8dBMIs0enEpoq0ke/IjSbleIrGQNZIMRX9EwZQ==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/quadrat-analysis": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/quadrat-analysis/-/quadrat-analysis-7.2.0.tgz", + "integrity": "sha512-fDQh3+ldYNxUqS6QYlvJ7GZLlCeDZR6tD3ikdYtOsSemwW1n/4gm2xcgWJqy3Y0uszBwxc13IGGY7NGEjHA+0w==", + "license": "MIT", + "dependencies": { + "@turf/area": "^7.2.0", + "@turf/bbox": "^7.2.0", + "@turf/bbox-polygon": "^7.2.0", + "@turf/centroid": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/point-grid": "^7.2.0", + "@turf/random": "^7.2.0", + "@turf/square-grid": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/random": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/random/-/random-7.2.0.tgz", + "integrity": "sha512-fNXs5mOeXsrirliw84S8UCNkpm4RMNbefPNsuCTfZEXhcr1MuHMzq4JWKb4FweMdN1Yx2l/xcytkO0s71cJ50w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rectangle-grid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/rectangle-grid/-/rectangle-grid-7.2.0.tgz", + "integrity": "sha512-f0o5ifvy0Ml/nHDJzMNcuSk4h11aa3BfvQNnYQhLpuTQu03j/ICZNlzKTLxwjcUqvxADUifty7Z9CX5W6zky4A==", + "license": "MIT", + "dependencies": { + "@turf/boolean-intersects": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rewind": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-7.2.0.tgz", + "integrity": "sha512-SZpRAZiZsE22+HVz6pEID+ST25vOdpAMGk5NO1JeqzhpMALIkIGnkG+xnun2CfYHz7wv8/Z0ADiAvei9rkcQYA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-clockwise": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-bearing": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-bearing/-/rhumb-bearing-7.2.0.tgz", + "integrity": "sha512-jbdexlrR8X2ZauUciHx3tRwG+BXoMXke4B8p8/IgDlAfIrVdzAxSQN89FMzIKnjJ/kdLjo9bFGvb92bu31Etug==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-destination": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-destination/-/rhumb-destination-7.2.0.tgz", + "integrity": "sha512-U9OLgLAHlH4Wfx3fBZf3jvnkDjdTcfRan5eI7VPV1+fQWkOteATpzkiRjCvSYK575GljVwWBjkKca8LziGWitQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-distance": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-distance/-/rhumb-distance-7.2.0.tgz", + "integrity": "sha512-NsijTPON1yOc9tirRPEQQuJ5aQi7pREsqchQquaYKbHNWsexZjcDi4wnw2kM3Si4XjmgynT+2f7aXH7FHarHzw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/sample": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/sample/-/sample-7.2.0.tgz", + "integrity": "sha512-f+ZbcbQJ9glQ/F26re8LadxO0ORafy298EJZe6XtbctRTJrNus6UNAsl8+GYXFqMnXM22tbTAznnJX3ZiWNorA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/sector": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/sector/-/sector-7.2.0.tgz", + "integrity": "sha512-zL06MjbbMG4DdpiNz+Q9Ax8jsCekt3R76uxeWShulAGkyDB5smdBOUDoRwxn05UX7l4kKv4Ucq2imQXhxKFd1w==", + "license": "MIT", + "dependencies": { + "@turf/circle": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/line-arc": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/shortest-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/shortest-path/-/shortest-path-7.2.0.tgz", + "integrity": "sha512-6fpx8feZ2jMSaeRaFdqFShGWkNb+veUOeyLFSHA/aRD9n/e9F2pWZoRbQWKbKTpcKFJ2FnDEqCZnh/GrcAsqWA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/bbox-polygon": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/clean-coords": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/transform-scale": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/simplify": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/simplify/-/simplify-7.2.0.tgz", + "integrity": "sha512-9YHIfSc8BXQfi5IvEMbCeQYqNch0UawIGwbboJaoV8rodhtk6kKV2wrpXdGqk/6Thg6/RWvChJFKVVTjVrULyQ==", + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/square": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/square/-/square-7.2.0.tgz", + "integrity": "sha512-9pMoAGFvqzCDOlO9IRSSBCGXKbl8EwMx6xRRBMKdZgpS0mZgfm9xiptMmx/t1m4qqHIlb/N+3MUF7iMBx6upcA==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/square-grid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/square-grid/-/square-grid-7.2.0.tgz", + "integrity": "sha512-EmzGXa90hz+tiCOs9wX+Lak6pH0Vghb7QuX6KZej+pmWi3Yz7vdvQLmy/wuN048+wSkD5c8WUo/kTeNDe7GnmA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/rectangle-grid": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/standard-deviational-ellipse": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/standard-deviational-ellipse/-/standard-deviational-ellipse-7.2.0.tgz", + "integrity": "sha512-+uC0pR2nRjm90JvMXe/2xOCZsYV2II1ZZ2zmWcBWv6bcFXBspcxk2QfCC3k0bj6jDapELzoQgnn3cG5lbdQV2w==", + "license": "MIT", + "dependencies": { + "@turf/center-mean": "^7.2.0", + "@turf/ellipse": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/points-within-polygon": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tag": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/tag/-/tag-7.2.0.tgz", + "integrity": "sha512-TAFvsbp5TCBqXue8ui+CtcLsPZ6NPC88L8Ad6Hb/R6VAi21qe0U42WJHQYXzWmtThoTNwxi+oKSeFbRDsr0FIA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tesselate": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/tesselate/-/tesselate-7.2.0.tgz", + "integrity": "sha512-zHGcG85aOJJu1seCm+CYTJ3UempX4Xtyt669vFG6Hbr/Hc7ii6STQ2ysFr7lJwFtU9uyYhphVrrgwIqwglvI/Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "earcut": "^2.2.4", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tin": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/tin/-/tin-7.2.0.tgz", + "integrity": "sha512-y24Vt3oeE6ZXvyLJamP0Ke02rPlDGE9gF7OFADnR0mT+2uectb0UTIBC3kKzON80TEAlA3GXpKFkCW5Fo/O/Kg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-rotate": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/transform-rotate/-/transform-rotate-7.2.0.tgz", + "integrity": "sha512-EMCj0Zqy3cF9d3mGRqDlYnX2ZBXe3LgT+piDR0EuF5c5sjuKErcFcaBIsn/lg1gp4xCNZFinkZ3dsFfgGHf6fw==", + "license": "MIT", + "dependencies": { + "@turf/centroid": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/rhumb-bearing": "^7.2.0", + "@turf/rhumb-destination": "^7.2.0", + "@turf/rhumb-distance": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-scale": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/transform-scale/-/transform-scale-7.2.0.tgz", + "integrity": "sha512-HYB+pw938eeI8s1/zSWFy6hq+t38fuUaBb0jJsZB1K9zQ1WjEYpPvKF/0//80zNPlyxLv3cOkeBucso3hzI07A==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "^7.2.0", + "@turf/center": "^7.2.0", + "@turf/centroid": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/rhumb-bearing": "^7.2.0", + "@turf/rhumb-destination": "^7.2.0", + "@turf/rhumb-distance": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-translate": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/transform-translate/-/transform-translate-7.2.0.tgz", + "integrity": "sha512-zAglR8MKCqkzDTjGMIQgbg/f+Q3XcKVzr9cELw5l9CrS1a0VTSDtBZLDm0kWx0ankwtam7ZmI2jXyuQWT8Gbug==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/rhumb-destination": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/triangle-grid": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/triangle-grid/-/triangle-grid-7.2.0.tgz", + "integrity": "sha512-4gcAqWKh9hg6PC5nNSb9VWyLgl821cwf9yR9yEzQhEFfwYL/pZONBWCO1cwVF23vSYMSMm+/TwqxH4emxaArfw==", + "license": "MIT", + "dependencies": { + "@turf/distance": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/intersect": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/truncate": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/truncate/-/truncate-7.2.0.tgz", + "integrity": "sha512-jyFzxYbPugK4XjV5V/k6Xr3taBjjvo210IbPHJXw0Zh7Y6sF+hGxeRVtSuZ9VP/6oRyqAOHKUrze+OOkPqBgUg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/turf": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/turf/-/turf-7.2.0.tgz", + "integrity": "sha512-G1kKBu4hYgoNoRJgnpJohNuS7bLnoWHZ2G/4wUMym5xOSiYah6carzdTEsMoTsauyi7ilByWHx5UHwbjjCVcBw==", + "license": "MIT", + "dependencies": { + "@turf/along": "^7.2.0", + "@turf/angle": "^7.2.0", + "@turf/area": "^7.2.0", + "@turf/bbox": "^7.2.0", + "@turf/bbox-clip": "^7.2.0", + "@turf/bbox-polygon": "^7.2.0", + "@turf/bearing": "^7.2.0", + "@turf/bezier-spline": "^7.2.0", + "@turf/boolean-clockwise": "^7.2.0", + "@turf/boolean-concave": "^7.2.0", + "@turf/boolean-contains": "^7.2.0", + "@turf/boolean-crosses": "^7.2.0", + "@turf/boolean-disjoint": "^7.2.0", + "@turf/boolean-equal": "^7.2.0", + "@turf/boolean-intersects": "^7.2.0", + "@turf/boolean-overlap": "^7.2.0", + "@turf/boolean-parallel": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/boolean-point-on-line": "^7.2.0", + "@turf/boolean-touches": "^7.2.0", + "@turf/boolean-valid": "^7.2.0", + "@turf/boolean-within": "^7.2.0", + "@turf/buffer": "^7.2.0", + "@turf/center": "^7.2.0", + "@turf/center-mean": "^7.2.0", + "@turf/center-median": "^7.2.0", + "@turf/center-of-mass": "^7.2.0", + "@turf/centroid": "^7.2.0", + "@turf/circle": "^7.2.0", + "@turf/clean-coords": "^7.2.0", + "@turf/clone": "^7.2.0", + "@turf/clusters": "^7.2.0", + "@turf/clusters-dbscan": "^7.2.0", + "@turf/clusters-kmeans": "^7.2.0", + "@turf/collect": "^7.2.0", + "@turf/combine": "^7.2.0", + "@turf/concave": "^7.2.0", + "@turf/convex": "^7.2.0", + "@turf/destination": "^7.2.0", + "@turf/difference": "^7.2.0", + "@turf/dissolve": "^7.2.0", + "@turf/distance": "^7.2.0", + "@turf/distance-weight": "^7.2.0", + "@turf/ellipse": "^7.2.0", + "@turf/envelope": "^7.2.0", + "@turf/explode": "^7.2.0", + "@turf/flatten": "^7.2.0", + "@turf/flip": "^7.2.0", + "@turf/geojson-rbush": "^7.2.0", + "@turf/great-circle": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/hex-grid": "^7.2.0", + "@turf/interpolate": "^7.2.0", + "@turf/intersect": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@turf/isobands": "^7.2.0", + "@turf/isolines": "^7.2.0", + "@turf/kinks": "^7.2.0", + "@turf/length": "^7.2.0", + "@turf/line-arc": "^7.2.0", + "@turf/line-chunk": "^7.2.0", + "@turf/line-intersect": "^7.2.0", + "@turf/line-offset": "^7.2.0", + "@turf/line-overlap": "^7.2.0", + "@turf/line-segment": "^7.2.0", + "@turf/line-slice": "^7.2.0", + "@turf/line-slice-along": "^7.2.0", + "@turf/line-split": "^7.2.0", + "@turf/line-to-polygon": "^7.2.0", + "@turf/mask": "^7.2.0", + "@turf/meta": "^7.2.0", + "@turf/midpoint": "^7.2.0", + "@turf/moran-index": "^7.2.0", + "@turf/nearest-neighbor-analysis": "^7.2.0", + "@turf/nearest-point": "^7.2.0", + "@turf/nearest-point-on-line": "^7.2.0", + "@turf/nearest-point-to-line": "^7.2.0", + "@turf/planepoint": "^7.2.0", + "@turf/point-grid": "^7.2.0", + "@turf/point-on-feature": "^7.2.0", + "@turf/point-to-line-distance": "^7.2.0", + "@turf/point-to-polygon-distance": "^7.2.0", + "@turf/points-within-polygon": "^7.2.0", + "@turf/polygon-smooth": "^7.2.0", + "@turf/polygon-tangents": "^7.2.0", + "@turf/polygon-to-line": "^7.2.0", + "@turf/polygonize": "^7.2.0", + "@turf/projection": "^7.2.0", + "@turf/quadrat-analysis": "^7.2.0", + "@turf/random": "^7.2.0", + "@turf/rectangle-grid": "^7.2.0", + "@turf/rewind": "^7.2.0", + "@turf/rhumb-bearing": "^7.2.0", + "@turf/rhumb-destination": "^7.2.0", + "@turf/rhumb-distance": "^7.2.0", + "@turf/sample": "^7.2.0", + "@turf/sector": "^7.2.0", + "@turf/shortest-path": "^7.2.0", + "@turf/simplify": "^7.2.0", + "@turf/square": "^7.2.0", + "@turf/square-grid": "^7.2.0", + "@turf/standard-deviational-ellipse": "^7.2.0", + "@turf/tag": "^7.2.0", + "@turf/tesselate": "^7.2.0", + "@turf/tin": "^7.2.0", + "@turf/transform-rotate": "^7.2.0", + "@turf/transform-scale": "^7.2.0", + "@turf/transform-translate": "^7.2.0", + "@turf/triangle-grid": "^7.2.0", + "@turf/truncate": "^7.2.0", + "@turf/union": "^7.2.0", + "@turf/unkink-polygon": "^7.2.0", + "@turf/voronoi": "^7.2.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/union": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/union/-/union-7.2.0.tgz", + "integrity": "sha512-Xex/cfKSmH0RZRWSJl4RLlhSmEALVewywiEXcu0aIxNbuZGTcpNoI0h4oLFrE/fUd0iBGFg/EGLXRL3zTfpg6g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/unkink-polygon": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/unkink-polygon/-/unkink-polygon-7.2.0.tgz", + "integrity": "sha512-dFPfzlIgkEr15z6oXVxTSWshWi51HeITGVFtl1GAKGMtiXJx1uMqnfRsvljqEjaQu/4AzG1QAp3b+EkSklQSiQ==", + "license": "MIT", + "dependencies": { + "@turf/area": "^7.2.0", + "@turf/boolean-point-in-polygon": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/meta": "^7.2.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/voronoi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@turf/voronoi/-/voronoi-7.2.0.tgz", + "integrity": "sha512-3K6N0LtJsWTXxPb/5N2qD9e8f4q8+tjTbGV3lE3v8x06iCnNlnuJnqM5NZNPpvgvCatecBkhClO3/3RndE61Fw==", + "license": "MIT", + "dependencies": { + "@turf/clone": "^7.2.0", + "@turf/helpers": "^7.2.0", + "@turf/invariant": "^7.2.0", + "@types/d3-voronoi": "^1.1.12", + "@types/geojson": "^7946.0.10", + "d3-voronoi": "1.1.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1702,6 +4365,18 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/d3-voronoi": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", + "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==", + "license": "MIT" + }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1709,6 +4384,46 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1720,16 +4435,21 @@ "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/react": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", - "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "version": "19.0.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", + "integrity": "sha512-V6Ar115dBDrjbtXSrS+/Oruobc+qVbbUxDFC1RSbRqLt5SYvxxyIDrSC85RWml54g+jfNeEMZhEj7wW07ONQhA==", "dev": true, "license": "MIT", "dependencies": { @@ -1746,6 +4466,81 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@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/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/stats.js": { + "version": "0.17.3", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", + "integrity": "sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==", + "license": "MIT" + }, + "node_modules/@types/three": { + "version": "0.174.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.174.0.tgz", + "integrity": "sha512-De/+vZnfg2aVWNiuy1Ldu+n2ydgw1osinmiZTAn0necE++eOfsygL8JpZgFjR2uHmAPo89MkxBj3JJ+2BMe+Uw==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "~23.1.3", + "@types/stats.js": "*", + "@types/webxr": "*", + "@webgpu/types": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "node_modules/@types/webxr": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.21.tgz", + "integrity": "sha512-geZIAtLzjGmgY2JUi6VxXdCrTb99A7yP49lxLr2Nm/uIK0PkkxcEi4OGhoGDO4pxCf3JwGz2GiJL2Ej4K2bKaA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "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": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.26.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.1.tgz", @@ -1965,6 +4760,24 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT" + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -1985,6 +4798,24 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@webgpu/types": { + "version": "0.1.58", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.58.tgz", + "integrity": "sha512-+8+NBE17zrc1wS4FvZmmuGTpog5C2H6QC46RY2TTWNpnt15Xvp7dZoUHZQXvb8l5ddKwO8l1pDJa6XTnR4Al1Q==", + "license": "BSD-3-Clause" + }, + "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/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -2029,7 +4860,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2048,6 +4878,33 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "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/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/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2055,6 +4912,62 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2070,7 +4983,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2112,6 +5024,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-builder": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", @@ -2119,6 +5055,12 @@ "dev": true, "license": "MIT/X11" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2129,6 +5071,15 @@ "node": ">=6" } }, + "node_modules/camera-controls": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.10.0.tgz", + "integrity": "sha512-vBQ5Daxv4KRsn07U/VqkPxoqD8U+S++0oq5NLf4HevMuh/BDta3rg49e/P564AMzFPBePQeXDKOkiIezRgyDwg==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001704", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz", @@ -2154,7 +5105,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -2195,11 +5145,34 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2212,7 +5185,12 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "license": "MIT" }, "node_modules/colorjs.io": { @@ -2222,6 +5200,18 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2229,6 +5219,47 @@ "dev": true, "license": "MIT" }, + "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/concat-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concaveman": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/concaveman/-/concaveman-1.2.1.tgz", + "integrity": "sha512-PwZYKaM/ckQSa8peP5JpVr7IMJ4Nn/MHIaWUjP4be+KoZ7Botgs8seAZGpmaOM+UZXawcdYRao/px9ycrCihHw==", + "license": "ISC", + "dependencies": { + "point-in-polygon": "^1.1.0", + "rbush": "^3.0.1", + "robust-predicates": "^2.0.4", + "tinyqueue": "^2.0.3" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2245,11 +5276,28 @@ "node": ">=18" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2260,6 +5308,12 @@ "node": ">= 8" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -2267,11 +5321,31 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-geo": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.7.1.tgz", + "integrity": "sha512-O4AempWAr+P5qbk2bC2FuN/sDW4z+dN2wDf9QV3bxQt4M5HfOEeXLgJ/UKQW0+o1Dj8BE+L5kiDbdWUMjsmQpw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", + "integrity": "sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw==", + "license": "BSD-3-Clause" + }, "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==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2292,6 +5366,24 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-gpu": { + "version": "5.0.70", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", + "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==", + "license": "MIT", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2306,6 +5398,33 @@ "node": ">=0.10" } }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT" + }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==", + "license": "Apache-2.0" + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, "node_modules/electron-to-chromium": { "version": "1.5.118", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.118.tgz", @@ -2313,6 +5432,66 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "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/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/esbuild": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", @@ -2555,11 +5734,65 @@ "node": ">=0.10.0" } }, + "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/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -2606,6 +5839,19 @@ "dev": true, "license": "MIT" }, + "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/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2616,6 +5862,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2629,11 +5881,22 @@ "node": ">=16.0.0" } }, + "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/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -2680,6 +5943,15 @@ "dev": true, "license": "ISC" }, + "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/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2705,6 +5977,48 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-equality-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/geojson-equality-ts/-/geojson-equality-ts-1.0.2.tgz", + "integrity": "sha512-h3Ryq+0mCSN/7yLs0eDgrZhvc9af23o/QuC4aTiuuzP/MRCtd6mf5rLsLRY44jX0RPUfM8c4GqERQmlUxPGPoQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.14" + } + }, + "node_modules/geojson-polygon-self-intersections": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/geojson-polygon-self-intersections/-/geojson-polygon-self-intersections-1.2.1.tgz", + "integrity": "sha512-/QM1b5u2d172qQVO//9CGRa49jEmclKEsYOQmWP9ooEjj63tBM51m2805xsbxkzlEELQ2REgTf700gUhhlegxA==", + "license": "MIT", + "dependencies": { + "rbush": "^2.0.1" + } + }, + "node_modules/geojson-polygon-self-intersections/node_modules/quickselect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-1.1.1.tgz", + "integrity": "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ==", + "license": "ISC" + }, + "node_modules/geojson-polygon-self-intersections/node_modules/rbush": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-2.0.2.tgz", + "integrity": "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==", + "license": "MIT", + "dependencies": { + "quickselect": "^1.0.1" + } + }, + "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/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2731,6 +6045,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glsl-noise": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", + "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", + "license": "MIT" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2738,16 +6064,53 @@ "dev": true, "license": "MIT" }, + "node_modules/gsap": { + "version": "3.12.7", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.7.tgz", + "integrity": "sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "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.20", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.20.tgz", + "integrity": "sha512-uu0VXUK52JhihhnN/MVVo1lvqNNuhoxkonqgO3IpjvQiGpJBdIXMGkofjQb/j9zvV7a1SW8U9g1FslWx/1HOiQ==", + "license": "Apache-2.0" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2758,6 +6121,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immutable": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", @@ -2792,12 +6161,33 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "license": "ISC" }, + "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", @@ -2825,24 +6215,146 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" } }, + "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-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, + "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/its-fine": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", + "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "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", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2905,6 +6417,15 @@ "node": ">=6" } }, + "node_modules/jsts": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/jsts/-/jsts-2.7.1.tgz", + "integrity": "sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg==", + "license": "(EDL-1.0 OR EPL-1.0)", + "engines": { + "node": ">= 12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2915,6 +6436,46 @@ "json-buffer": "3.0.1" } }, + "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/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2929,6 +6490,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2945,6 +6515,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2952,6 +6528,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2962,6 +6550,37 @@ "yallist": "^3.0.2" } }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/marchingsquares": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/marchingsquares/-/marchingsquares-1.3.3.tgz", + "integrity": "sha512-gz6nNQoVK7Lkh2pZulrT4qd4347S/toG9RXH2pyzhLgkL5mLkBoqgv4EvAGXcV0ikDW72n/OQb3Xe8bGagQZCg==", + "license": "AGPL-3.0" + }, + "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", @@ -2972,11 +6591,25 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz", + "integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==", + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -2986,6 +6619,15 @@ "node": ">=8.6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2999,13 +6641,93 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/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/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/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, + "node_modules/n8ao": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.9.4.tgz", + "integrity": "sha512-gbpAorQecZn2oGK/rheHxPTNwOxVsEC6216+Jr9tXHUk9L5VCE2q/uxsSrQpfNkZDoCmQHf7oSg3SYFWCAt0wg==", + "license": "ISC", + "peerDependencies": { + "postprocessing": ">=6.30.0", + "three": ">=0.137" + } + }, "node_modules/nanoid": { "version": "3.3.9", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz", @@ -3047,6 +6769,25 @@ "dev": true, "license": "MIT" }, + "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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3134,7 +6875,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3144,14 +6884,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -3160,6 +6898,37 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, + "node_modules/point-in-polygon-hao": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz", + "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==", + "license": "MIT", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/point-in-polygon-hao/node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/polyclip-ts": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/polyclip-ts/-/polyclip-ts-0.16.8.tgz", + "integrity": "sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.1.0", + "splaytree-ts": "^1.0.2" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -3189,6 +6958,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postprocessing": { + "version": "6.37.1", + "resolved": "https://registry.npmjs.org/postprocessing/-/postprocessing-6.37.1.tgz", + "integrity": "sha512-fZszlSB8j+PaxtS8g4qMxdj+ifzvoCPnbHSOjclTlr4mbhd6/huQqOViM6lhhPIrW2fiZc+IRcnReoKYvyMwNg==", + "license": "Zlib", + "peerDependencies": { + "three": ">= 0.157.0 < 0.175.0" + } + }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3199,6 +6983,32 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3208,6 +7018,39 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/promise-worker-transferable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", + "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==", + "license": "Apache-2.0", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3239,6 +7082,21 @@ ], "license": "MIT" }, + "node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, "node_modules/react": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", @@ -3258,6 +7116,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-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", @@ -3270,6 +7138,44 @@ "react": "^19.0.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-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-reconciler": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz", + "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.25.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -3320,6 +7226,50 @@ "react-dom": ">=18" } }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "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/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3334,6 +7284,40 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3355,6 +7339,18 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/robust-predicates": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz", + "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.35.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.35.0.tgz", @@ -3428,6 +7424,12 @@ "tslib": "^2.1.0" } }, + "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/sass": { "version": "1.85.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", @@ -3872,11 +7874,34 @@ "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "license": "MIT" }, + "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/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3889,12 +7914,88 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/skmeans": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", + "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3905,6 +8006,128 @@ "node": ">=0.10.0" } }, + "node_modules/splaytree-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/splaytree-ts/-/splaytree-ts-1.0.2.tgz", + "integrity": "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==", + "license": "BDS-3-Clause" + }, + "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/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stats-gl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz", + "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==", + "license": "MIT", + "dependencies": { + "@types/three": "*", + "three": "^0.170.0" + }, + "peerDependencies": { + "@types/three": "*", + "three": "*" + } + }, + "node_modules/stats.js": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz", + "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3922,7 +8145,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -3931,6 +8153,24 @@ "node": ">=8" } }, + "node_modules/suspend-react": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", + "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0" + } + }, + "node_modules/sweepline-intersections": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sweepline-intersections/-/sweepline-intersections-1.5.0.tgz", + "integrity": "sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ==", + "license": "MIT", + "dependencies": { + "tinyqueue": "^2.0.0" + } + }, "node_modules/sync-child-process": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", @@ -3954,11 +8194,64 @@ "node": ">=16.0.0" } }, + "node_modules/three": { + "version": "0.170.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz", + "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==", + "license": "MIT" + }, + "node_modules/three-bvh-csg": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/three-bvh-csg/-/three-bvh-csg-0.0.16.tgz", + "integrity": "sha512-RgC5dY0hAKdfd1bmD3o2CDfmK9GTkvsA1ECzoqTMhSkjSc2zp1z4Wpa5+emLi/EosF5P6+aK2veTxTLZA9+Mhw==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.151.0", + "three-mesh-bvh": ">=0.6.6" + } + }, + "node_modules/three-mesh-bvh": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.6.8.tgz", + "integrity": "sha512-EGebF9DZx1S8+7OZYNNTT80GXJZVf+UYXD/HyTg/e2kR/ApofIFfUS4ZzIHNnUVIadpnLSzM4n96wX+l7GMbnQ==", + "license": "MIT", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.35.14", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.35.14.tgz", + "integrity": "sha512-kpCaEg59M9usFTgHC+YZNKvx7nMoLI2zQxZBV8pjoNW6vNZmGyXpaLBL09A2oLCsS3KepgMFkOuk6lRoebTNvA==", + "license": "MIT", + "dependencies": { + "@types/draco3d": "^1.4.0", + "@types/offscreencanvas": "^2019.6.4", + "@types/webxr": "^0.5.2", + "draco3d": "^1.4.1", + "fflate": "^0.6.9", + "potpack": "^1.0.1" + }, + "peerDependencies": { + "three": ">=0.128.0" + } + }, + "node_modules/three-stdlib/node_modules/fflate": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz", + "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", + "license": "MIT" + }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "license": "ISC" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -3967,6 +8260,62 @@ "node": ">=8.0" } }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-server": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/topojson-server/-/topojson-server-3.0.1.tgz", + "integrity": "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "geo2topo": "bin/geo2topo" + } + }, + "node_modules/troika-three-text": { + "version": "0.52.3", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.3.tgz", + "integrity": "sha512-jLhiwgV8kEkwWjvK12f2fHVpbOC75p7SgPQ0cgcz+IMtN5Bdyg4EuFdwuTOVu9ga8UeYdKBpzd1AxviyixtYTQ==", + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.52.0", + "troika-worker-utils": "^0.52.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.0.tgz", + "integrity": "sha512-00oxqIIehtEKInOTQekgyknBuRUj1POfOUE2q1OmL+Xlpp4gIu+S0oA0schTyXsDS4d9DkR04iqCdD40rF5R6w==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz", + "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", @@ -3984,9 +8333,45 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, + "node_modules/tunnel-rat": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", + "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==", + "license": "MIT", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz", + "integrity": "sha512-ibr/n1hBzLLj5Y+yUcU7dYw8p6WnIVzdJbnX+1YpaScvZVF2ziugqHs+LAmHw4lWO9c/zRj+K1ncgWDQuthEdQ==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/turbo-stream": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", @@ -4006,6 +8391,12 @@ "node": ">= 0.8.0" } }, + "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/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -4047,7 +8438,6 @@ "version": "6.20.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -4091,6 +8481,29 @@ "punycode": "^2.1.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.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -4100,6 +8513,27 @@ "inherits": "2.0.3" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utility-types": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "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/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", @@ -4179,11 +8613,21 @@ } } }, + "node_modules/webgl-constants": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz", + "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==" + }, + "node_modules/webgl-sdf-generator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz", + "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4205,6 +8649,69 @@ "node": ">=0.10.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/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/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/app/package.json b/app/package.json index 3f3d446..6d53a7e 100644 --- a/app/package.json +++ b/app/package.json @@ -10,18 +10,34 @@ "preview": "vite preview" }, "dependencies": { + "@react-three/csg": "^4.0.0", + "@react-three/drei": "^10.0.4", + "@react-three/fiber": "^9.1.0", + "@react-three/postprocessing": "^3.0.4", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.2.0", + "@testing-library/user-event": "^14.6.1", + "@turf/turf": "^7.2.0", + "@types/jest": "^29.5.14", + "@use-gesture/react": "^10.3.1", "chart.js": "^4.4.8", + "gsap": "^3.12.7", + "leva": "^0.10.0", + "mqtt": "^5.10.4", "path": "^0.12.7", + "postprocessing": "^6.37.1", "react": "^19.0.0", "react-chartjs-2": "^5.3.0", "react-dom": "^19.0.0", "react-router-dom": "^7.3.0", + "react-toastify": "^11.0.5", + "socket.io-client": "^4.8.1", "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.21.0", "@types/node": "^22.13.10", - "@types/react": "^19.0.10", + "@types/react": "^19.0.12", "@types/react-dom": "^19.0.4", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.21.0", diff --git a/app/src/App.tsx b/app/src/App.tsx index 2b06dd3..188685d 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,19 +1,24 @@ -import React from 'react'; -import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; -import Dashboard from './pages/Dashboard'; -import Project from './pages/Project'; -import UserAuth from './pages/UserAuth'; +import React from "react"; +import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import Dashboard from "./pages/Dashboard"; +import Project from "./pages/Project"; +import UserAuth from "./pages/UserAuth"; +import ToastProvider from "./components/templates/ToastProvider"; const App: React.FC = () => { - console.log(""); return ( - - - } /> - } /> - } /> - - + + + + } + /> + } /> + } /> + + + ); }; diff --git a/app/src/components/icons/ExportCommonIcons.tsx b/app/src/components/icons/ExportCommonIcons.tsx index e64af49..dd170f8 100644 --- a/app/src/components/icons/ExportCommonIcons.tsx +++ b/app/src/components/icons/ExportCommonIcons.tsx @@ -326,22 +326,42 @@ export function FilterIcon() { ); } -export function EyeDroperIcon() { +export function EyeDroperIcon({ isActive }: { isActive: boolean }) { return ( + @@ -457,10 +477,37 @@ export function InfoIcon() { ); } + +export function AI_Icon() { + return ( + + + + + + ); +} diff --git a/app/src/components/icons/ExportToolsIcons.tsx b/app/src/components/icons/ExportToolsIcons.tsx index d463c0a..9e2c937 100644 --- a/app/src/components/icons/ExportToolsIcons.tsx +++ b/app/src/components/icons/ExportToolsIcons.tsx @@ -136,7 +136,7 @@ export function WallIcon({ isActive }: { isActive: boolean }) { d="M12.6101 7.17339H6.72171V10.7064H12.6101V7.17339Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" /> @@ -144,7 +144,7 @@ export function WallIcon({ isActive }: { isActive: boolean }) { d="M18.4986 7.17339H12.6102V10.7064H18.4986V7.17339Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" /> @@ -152,7 +152,7 @@ export function WallIcon({ isActive }: { isActive: boolean }) { d="M6.72172 7.17339H0.833313V10.7064H6.72172V7.17339Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" /> @@ -160,7 +160,7 @@ export function WallIcon({ isActive }: { isActive: boolean }) { d="M15.5544 10.7064H9.66595V14.2395H15.5544V10.7064Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" /> @@ -168,7 +168,7 @@ export function WallIcon({ isActive }: { isActive: boolean }) { d="M9.66594 10.7064H3.77753V14.2395H9.66594V10.7064Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" /> @@ -176,7 +176,7 @@ export function WallIcon({ isActive }: { isActive: boolean }) { d="M15.5544 3.64035H9.66595V7.1734H15.5544V3.64035Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" /> @@ -184,7 +184,7 @@ export function WallIcon({ isActive }: { isActive: boolean }) { d="M9.66594 3.64035H3.77753V7.1734H9.66594V3.64035Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" strokeLinecap="round" strokeLinejoin="round" /> @@ -213,49 +213,49 @@ export function WallIcon({ isActive }: { isActive: boolean }) { @@ -380,7 +380,7 @@ export function PillerIcon({ isActive }: { isActive: boolean }) { d="M14.7545 1.94309H5.22935C4.9007 1.94074 4.57782 2.04074 4.29614 2.23213C4.01447 2.42352 3.78489 2.69889 3.63259 3.02804C3.4803 3.35718 3.41117 3.72738 3.4328 4.098C3.45444 4.46862 3.56599 4.82535 3.75522 5.12904C3.94445 5.43272 4.20405 5.67163 4.50553 5.81955C4.807 5.96747 5.13871 6.01868 5.46425 5.96756C5.78978 5.91644 6.09657 5.76497 6.35094 5.52976C6.60531 5.29456 6.79744 4.98471 6.90624 4.63423H13.1088C13.2185 4.98209 13.4107 5.2892 13.6644 5.522C13.918 5.75479 14.2234 5.90432 14.5472 5.95425C14.871 6.00417 15.2007 5.95258 15.5003 5.80509C15.7999 5.65761 16.0578 5.41991 16.246 5.11797C16.4341 4.81603 16.5452 4.46146 16.5671 4.093C16.589 3.72454 16.5209 3.35636 16.3702 3.02869C16.2195 2.70102 15.992 2.42646 15.7125 2.235C15.4331 2.04355 15.1125 1.94257 14.7857 1.94309H14.7545Z" fill="var(--highlight-accent-color)" stroke="var(--accent-color)" - stroke-miterlimit="10" + strokeMiterlimit="10" /> ) : ( @@ -394,17 +394,17 @@ export function PillerIcon({ isActive }: { isActive: boolean }) { ); diff --git a/app/src/components/icons/Logo.tsx b/app/src/components/icons/Logo.tsx index 7053681..06ed750 100644 --- a/app/src/components/icons/Logo.tsx +++ b/app/src/components/icons/Logo.tsx @@ -114,3 +114,102 @@ export function LogoIcon() { ); } + +export function LogoIconLarge() { + return ( + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/src/components/layout/sidebarRight/SideBarRight.tsx b/app/src/components/layout/sidebarRight/SideBarRight.tsx index 8051eb7..88fe6bc 100644 --- a/app/src/components/layout/sidebarRight/SideBarRight.tsx +++ b/app/src/components/layout/sidebarRight/SideBarRight.tsx @@ -9,6 +9,9 @@ import { import useToggleStore from "../../../store/useUIToggleStore"; import MachineMechanics from "./mechanics/MachineMechanics"; import Visualization from "./visualization/Visualization"; +import GlobalProperties from "./properties/GlobalProperties"; +import AsstePropertiies from "./properties/AssetProperties"; +import Analysis from "./analysis/Analysis"; const SideBarRight: React.FC = () => { const { activeModule } = useModuleStore(); @@ -56,12 +59,36 @@ const SideBarRight: React.FC = () => { )} {/* process builder */} - {toggleUI && activeModule === "simulation" && ( -
-
- + {toggleUI && + activeList === "properties" && + activeModule !== "visualization" && ( +
+
+ + {/* */} +
-
+ )} + + {/* simulation */} + + {toggleUI && activeModule === "simulation" && ( + <> + {activeList === "mechanics" && ( +
+
+ +
+
+ )} + {activeList === "analysis" && ( +
+
+ +
+
+ )} + )} {/* realtime visualization */} diff --git a/app/src/components/layout/sidebarRight/analysis/Analysis.tsx b/app/src/components/layout/sidebarRight/analysis/Analysis.tsx new file mode 100644 index 0000000..8503bb9 --- /dev/null +++ b/app/src/components/layout/sidebarRight/analysis/Analysis.tsx @@ -0,0 +1,143 @@ +import React, { useState } from "react"; +import { AI_Icon } from "../../../icons/ExportCommonIcons"; +import RegularDropDown from "../../../ui/inputs/RegularDropDown"; +import { AnalysisPresetsType } from "../../../../types/analysis"; +import RenderAnalysisInputs from "./RenderAnalysisInputs"; + +const Analysis: React.FC = () => { + const [selectedOption, setSelectedOption] = useState("Throughput time"); + + const handleSelect = (option: string) => { + setSelectedOption(option); // Normalize for key matching + }; + + const AnalysisPresets: AnalysisPresetsType = { + "Throughput time": [ + { type: "default", inputs: { label: "Height", activeOption: "mm" } }, + { type: "default", inputs: { label: "Width", activeOption: "mm" } }, + { type: "default", inputs: { label: "Length", activeOption: "mtr" } }, + { type: "default", inputs: { label: "Thickness", activeOption: "mm" } }, + { + type: "default", + inputs: { label: "Raw Material", activeOption: "mm" }, + }, + { + type: "range", + inputs: { label: "Material flow", activeOption: "m/min" }, + }, + ], + "Production capacity": [ + { type: "default", inputs: { label: "Height", activeOption: "mm" } }, + { type: "default", inputs: { label: "Width", activeOption: "mm" } }, + { type: "default", inputs: { label: "Length", activeOption: "mtr" } }, + { type: "default", inputs: { label: "Thickness", activeOption: "mm" } }, + { + type: "default", + inputs: { label: "Raw Material", activeOption: "mm" }, + }, + { + type: "range", + inputs: { label: "Material flow", activeOption: "m/min" }, + }, + { + type: "range", + inputs: { label: "Shift 1", activeOption: "hr(s)" }, + }, + { + type: "range", + inputs: { label: "Shift 2", activeOption: "hr(s)" }, + }, + { + type: "range", + inputs: { label: "Over time", activeOption: "hr(s)" }, + }, + ], + ROI: [ + { + type: "default", + inputs: { label: "Equipment Cost", activeOption: "INR" }, + }, + { + type: "default", + inputs: { label: "Employee Salary", activeOption: "INR" }, + }, + { + type: "default", + inputs: { label: "Labor Cost", activeOption: "INR" }, + }, + { + type: "default", + inputs: { label: "Cost per unit", activeOption: "INR" }, + }, + { + type: "default", + inputs: { label: "Electricity cost", activeOption: "INR" }, + }, + { + type: "default", + inputs: { label: "Upkeep Cost", activeOption: "INR" }, + }, + { + type: "default", + inputs: { label: "Working Hours", activeOption: "Hrs" }, + }, + { + type: "default", + inputs: { label: "Power Usage", activeOption: "KWH" }, + }, + { type: "default", inputs: { label: "KWH", activeOption: "Mos" } }, + { + type: "default", + inputs: { label: "Man Power", activeOption: "Person" }, + }, + ], + }; + + return ( +
+
+
Object
+
+ Generate Report +
+
+
+
Create Analysis
+
+
+ +
+ {/* Render only the selected option */} + +
+ + +
+
+
Create Custom Analysis
+
+ Click 'Create' to enhances decision-making by + providing actionable insights, optimizing operations that adapts + to the unique challenges. +
+
+ +
+
+
+
+
+ ); +}; + +export default Analysis; diff --git a/app/src/components/layout/sidebarRight/analysis/RenderAnalysisInputs.tsx b/app/src/components/layout/sidebarRight/analysis/RenderAnalysisInputs.tsx new file mode 100644 index 0000000..aea58eb --- /dev/null +++ b/app/src/components/layout/sidebarRight/analysis/RenderAnalysisInputs.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; +import InputRange from "../../../ui/inputs/InputRange"; +import { AnalysisPresetsType } from "../../../../types/analysis"; + +interface InputRendererProps { + keyName: string; + presets: AnalysisPresetsType[keyof AnalysisPresetsType]; +} + +const RenderAnalysisInputs: React.FC = ({ + keyName, + presets, +}) => { + return ( +
+ {presets.map((preset, index) => { + if (preset.type === "default") { + return ( + {}} + /> + ); + } + if (preset.type === "range") { + return ; + } + return null; + })} +
+ ); +}; + +export default RenderAnalysisInputs; diff --git a/app/src/components/layout/sidebarRight/customInput/PositionInputs.tsx b/app/src/components/layout/sidebarRight/customInput/PositionInputs.tsx new file mode 100644 index 0000000..b65d782 --- /dev/null +++ b/app/src/components/layout/sidebarRight/customInput/PositionInputs.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +interface PositionInputProps { + onChange: (value: string) => void; // Callback for value change + placeholder?: string; // Optional placeholder + type?: string; // Input type (e.g., text, number, email) +} + +const PositionInput: React.FC = ({ + onChange, + placeholder = "Enter value", // Default placeholder + type = "number", // Default type +}) => { + return ( +
+
Position
+
+
+
X :
+ onChange(e.target.value)} + placeholder={placeholder} + /> +
+
+
Y :
+ onChange(e.target.value)} + placeholder={placeholder} + /> +
+
+
+ ); +}; + +export default PositionInput; diff --git a/app/src/components/layout/sidebarRight/customInput/RotationInput.tsx b/app/src/components/layout/sidebarRight/customInput/RotationInput.tsx new file mode 100644 index 0000000..3ab01a4 --- /dev/null +++ b/app/src/components/layout/sidebarRight/customInput/RotationInput.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +interface RotationInputProps { + onChange: (value: string) => void; // Callback for value change + placeholder?: string; // Optional placeholder + type?: string; // Input type (e.g., text, number, email) +} + +const RotationInput: React.FC = ({ + onChange, + placeholder = "Enter value", // Default placeholder + type = "number", // Default type +}) => { + return ( +
+
Rotation
+
+
+
Rotate :
+ onChange(e.target.value)} + placeholder={placeholder} + /> +
+
+
+ ); +}; + +export default RotationInput; diff --git a/app/src/components/layout/sidebarRight/mechanics/MachineMechanics.tsx b/app/src/components/layout/sidebarRight/mechanics/MachineMechanics.tsx index 803c96a..9aa27bb 100644 --- a/app/src/components/layout/sidebarRight/mechanics/MachineMechanics.tsx +++ b/app/src/components/layout/sidebarRight/mechanics/MachineMechanics.tsx @@ -10,6 +10,7 @@ import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; import LabledDropdown from "../../../ui/inputs/LabledDropdown"; import RegularDropDown from "../../../ui/inputs/RegularDropDown"; import { handleResize } from "../../../../functions/handleResizePannel"; +import EyeDropInput from "../../../ui/inputs/EyeDropInput"; const MachineMechanics: React.FC = () => { const [actionList, setActionList] = useState([]); @@ -54,16 +55,30 @@ const MachineMechanics: React.FC = () => { setSelectedItem({ type, name }); }; + const [processes, setProcesses] = useState([]); + const [activeProcess, setActiveProcesses] = useState(); + + const handleSelect = (option: string) => { + setActiveProcesses(option); // Update the active option state + }; + const handleAddProcess = () => { + const newProcess = `Process ${processes.length + 1}`; // Generate new process name dynamically + setProcesses((prevProcesses) => [...prevProcesses, newProcess]); // Update the state with the new process + }; + return (
Selected Object
Process:
{}} + header={activeProcess || "add process ->"} + options={processes} + onSelect={handleSelect} /> +
+ +
@@ -168,7 +183,13 @@ const MachineMechanics: React.FC = () => { defaultOption="On-hit" options={["On-hit", "Buffer"]} /> - + {}} + /> + )}
diff --git a/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx new file mode 100644 index 0000000..a3a0d28 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/AssetProperties.tsx @@ -0,0 +1,88 @@ +import React, { useState } from "react"; +import InputToggle from "../../../ui/inputs/InputToggle"; +import InputWithDropDown from "../../../ui/inputs/InputWithDropDown"; +import { RemoveIcon } from "../../../icons/ExportCommonIcons"; +import PositionInput from "../customInput/PositionInputs"; +import RotationInput from "../customInput/RotationInput"; + +interface UserData { + id: number; // Unique identifier for the user data + label: string; // Label of the user data field + value: string; // Value of the user data field +} + +const AssetProperties: React.FC = () => { + const [userData, setUserData] = useState([]); // State to track user data + const [nextId, setNextId] = useState(1); // Unique ID for new entries + + // Function to handle adding new user data + const handleAddUserData = () => { + const newUserData: UserData = { + id: nextId, + label: `Property ${nextId}`, + value: "", + }; + setUserData([...userData, newUserData]); + setNextId(nextId + 1); // Increment the ID for the next entry + }; + + // Function to update the value of a user data entry + const handleUserDataChange = (id: number, newValue: string) => { + setUserData((prevUserData) => + prevUserData.map((data) => + data.id === id ? { ...data, value: newValue } : data + ) + ); + }; + + // Remove user data + const handleRemoveUserData = (id: number) => { + setUserData((prevUserData) => + prevUserData.filter((data) => data.id !== id) + ); + }; + + return ( +
+
Selected Object
+ +
+ + {}} /> + {}} /> + +
+ + + + +
User Data
+ + {/* Map through userData and render InputWithDropDown for each entry */} + {userData.map((data) => ( +
+ handleUserDataChange(data.id, newValue)} // Pass the change handler + /> +
handleRemoveUserData(data.id)} + > + +
+
+ ))} + + {/* Add new user data */} +
+ + Add +
+
+ ); +}; + +export default AssetProperties; diff --git a/app/src/components/layout/sidebarRight/properties/GlobalProperties.tsx b/app/src/components/layout/sidebarRight/properties/GlobalProperties.tsx new file mode 100644 index 0000000..672d847 --- /dev/null +++ b/app/src/components/layout/sidebarRight/properties/GlobalProperties.tsx @@ -0,0 +1,81 @@ +import React, { useState } from "react"; +import InputRange from "../../../ui/inputs/InputRange"; +import InputToggle from "../../../ui/inputs/InputToggle"; +import { AI_Icon } from "../../../icons/ExportCommonIcons"; +import LabeledButton from "../../../ui/inputs/LabledButton"; + +const GlobalProperties: React.FC = () => { + const [limitDistance, setLimitDistance] = useState(false); + const [distance, setDistance] = useState(5); + + const [limitGridDistance, setLimitGridDistance] = useState(false); + const [gridDistance, setGridDistance] = useState(5); + + function optimizeScene() { + setLimitDistance(true); + setDistance(5); + } + + function updateDistance(value: number) { + setDistance(value); + } + + function updateGridDistance(value: number) { + setGridDistance(value); + } + + return ( +
+
Environment
+
+ + Optimize +
+ +
+ + + + + {}} value="Reset"/> + +
+ + { + setLimitDistance(!limitDistance); + }} + /> + updateDistance(value)} + /> + +
+ + { + setLimitGridDistance(!limitGridDistance); + }} + /> + updateGridDistance(value)} + /> +
+ ); +}; + +export default GlobalProperties; diff --git a/app/src/components/layout/sidebarRight/properties/Properties.tsx b/app/src/components/layout/sidebarRight/properties/Properties.tsx deleted file mode 100644 index dc33645..0000000 --- a/app/src/components/layout/sidebarRight/properties/Properties.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; - -const GlobalProperties: React.FC = () => { - return ( -
GlobalProperties
- ); -}; - -export default GlobalProperties; diff --git a/app/src/components/templates/CollaborationPopup.tsx b/app/src/components/templates/CollaborationPopup.tsx new file mode 100644 index 0000000..34e3669 --- /dev/null +++ b/app/src/components/templates/CollaborationPopup.tsx @@ -0,0 +1,128 @@ +import React, { useState } from "react"; +import RenderOverlay from "./Overlay"; +import { ArrowIcon, CloseIcon } from "../icons/ExportCommonIcons"; +import { AccessOption, User } from "../../types/users"; +import RegularDropDown from "../ui/inputs/RegularDropDown"; +import { access } from "fs"; +import MultiEmailInvite from "../ui/inputs/MultiEmailInvite"; + +interface UserListTemplateProps { + user: User; +} + +const UserListTemplate: React.FC = ({ user }) => { + const [accessSelection, setAccessSelection] = useState(user.access); + + function accessUpdate({ option }: AccessOption) { + setAccessSelection(option); + } + + return ( +
+
+
+ {user.profileImage ? ( + {`${user.name}'s + ) : ( +
+ {user.name[0]} +
+ )} +
+
{user.name}
+
+
+ accessUpdate({ option })} + search={false} + /> + {/* */} +
+
+ ); +}; + +const CollaborationPopup: React.FC = () => { + const users = [ + { + name: "Alice Johnson", + email: "alice.johnson@example.com", + profileImage: "", + color: "#FF6600", + access: "Admin", + }, + { + name: "Bob Smith", + email: "bob.smith@example.com", + profileImage: "", + color: "#488EF6", + access: "Viewer", + }, + { + name: "Charlie Brown", + email: "charlie.brown@example.com", + profileImage: "", + color: "#48AC2A", + access: "Viewer", + }, + { + name: "Diana Prince", + email: "diana.prince@example.com", + profileImage: "", + color: "#D44242", + access: "Viewer", + }, + ]; + + return ( + +
+
+
+
Share this file
+
+
copy link
+
+ +
+
+
+
+ +
+
+
+
Who has access
+
+
+
Untitled
+
+ {users.length} persons +
+
+
+
+
+
User 1
+
you
+
+ {users.map((user, index) => ( + + ))} +
+
+
+
+
+ ); +}; + +export default CollaborationPopup; diff --git a/app/src/components/templates/LoadingPage.tsx b/app/src/components/templates/LoadingPage.tsx new file mode 100644 index 0000000..523caf5 --- /dev/null +++ b/app/src/components/templates/LoadingPage.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import RenderOverlay from "./Overlay"; +import { LogoIconLarge } from "../icons/Logo"; + +interface LoadingPageProps { + progress: number; // Expect progress as a percentage (0-100) +} + +const LoadingPage: React.FC = ({ progress }) => { + // Ensure progress stays within 0-100 range + const validatedProgress = Math.min(100, Math.max(0, progress)); + + return ( + +
+
+
Untitled
+
+
+ +
+
Entering A New World of Dwinzo
+
+
+
{validatedProgress}%
+
+
+
+
+
+
+
+ ); +}; + +export default LoadingPage; diff --git a/app/src/components/templates/ToastProvider.tsx b/app/src/components/templates/ToastProvider.tsx new file mode 100644 index 0000000..4bb8207 --- /dev/null +++ b/app/src/components/templates/ToastProvider.tsx @@ -0,0 +1,74 @@ +import React, { createContext, useContext, useState, ReactNode } from "react"; + +type Toast = { + id: string; + message: string; + type: "success" | "error" | "info" | "warning"; + position?: ToastPosition; // Optional position for each toast +}; + +type ToastPosition = + | "top-left" + | "top-center" + | "top-right" + | "bottom-left" + | "bottom-center" + | "bottom-right"; + +type ToastContextType = { + addToast: ( + message: string, + type: Toast["type"], + position?: ToastPosition + ) => void; + removeToast: (id: string) => void; +}; + +const ToastContext = createContext(undefined); + +const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [toasts, setToasts] = useState([]); + + const addToast = ( + message: string, + type: Toast["type"], + position: ToastPosition = "bottom-center" // Default position + ) => { + const id = Math.random().toString(36).substr(2, 9); // Generate a unique ID + setToasts((prev) => [...prev, { id, message, type, position }]); + + // Auto-remove toast after 3 seconds + setTimeout(() => removeToast(id), 3000); + }; + + const removeToast = (id: string) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + return ( + + {children} +
+ {toasts.map((toast) => ( +
removeToast(toast.id)} // Allow manual dismissal + > + {toast.message} +
+ ))} +
+
+ ); +}; + +export const useToast = (): ToastContextType => { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used within a ToastProvider"); + } + return context; +}; + +export default ToastProvider; diff --git a/app/src/components/ui/3d-ui/distanceText.jsx b/app/src/components/ui/3d-ui/distanceText.jsx new file mode 100644 index 0000000..ba1847e --- /dev/null +++ b/app/src/components/ui/3d-ui/distanceText.jsx @@ -0,0 +1,89 @@ +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 "../scene/geomentries/lines/lineConvertions/objectLinesToArray"; +import { Html } from "@react-three/drei"; + + +const DistanceText = () => { + const [lines, setLines] = useState([]); + const { activeLayer } = useActiveLayer(); + const { toggleView } = useToggleView(); + const { newLines, setNewLines } = useNewLines(); + const { deletedLines, setDeletedLines } = useDeletedLines(); + + useEffect(() => { + const email = localStorage.getItem('email') + const organization = (email.split("@")[1]).split(".")[0]; + + getLines(organization).then((data) => { + data = objectLinesToArray(data); + + const lines = data.filter(line => line[0][2] === activeLayer) + .map(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) => { + 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.length > 0) { + setLines((prevLines) => + prevLines.filter( + (line) => !deletedLines.some((deletedLine) => 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/components/ui/3d-ui/functions/updateDistanceText.ts b/app/src/components/ui/3d-ui/functions/updateDistanceText.ts new file mode 100644 index 0000000..d0f7ede --- /dev/null +++ b/app/src/components/ui/3d-ui/functions/updateDistanceText.ts @@ -0,0 +1,42 @@ +import * as THREE from 'three'; + +import * as Types from "../../scene/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/components/ui/3d-ui/referenceDistanceText.jsx b/app/src/components/ui/3d-ui/referenceDistanceText.jsx new file mode 100644 index 0000000..3acfcce --- /dev/null +++ b/app/src/components/ui/3d-ui/referenceDistanceText.jsx @@ -0,0 +1,39 @@ +import * as THREE from 'three'; +import { Html } from '@react-three/drei'; +import { useState, useEffect } from 'react'; +import { useActiveLayer } from '../../store/store'; + +const ReferenceDistanceText = ({ line }) => { + const [text, setTexts] = useState(null); + const { activeLayer } = useActiveLayer(); + + useEffect(() => { + if (line === undefined || 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.length} 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/components/ui/inputs/EyeDropInput.tsx b/app/src/components/ui/inputs/EyeDropInput.tsx new file mode 100644 index 0000000..2823392 --- /dev/null +++ b/app/src/components/ui/inputs/EyeDropInput.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import RegularDropDown from "./RegularDropDown"; +import { EyeDroperIcon } from "../../icons/ExportCommonIcons"; + +const EyeDropInput: React.FC = () => { + return ( +
+
Object
+
+ {}} + /> +
+ +
+
+
+ ); +}; + +export default EyeDropInput; diff --git a/app/src/components/ui/inputs/InputRange.tsx b/app/src/components/ui/inputs/InputRange.tsx new file mode 100644 index 0000000..b46d9b4 --- /dev/null +++ b/app/src/components/ui/inputs/InputRange.tsx @@ -0,0 +1,69 @@ +import React, { useEffect, useState } from "react"; + +interface InputToggleProps { + label: string; // Represents the toggle state (on/off) + min?: number; + max?: number; + onClick?: () => void; // Function to handle toggle clicks + onChange?: (value: number) => void; // Function to handle toggle clicks + disabled?: boolean; + value?: number; +} + +const InputRange: React.FC = ({ + label, + onClick, + onChange, + min = 0, + max = 10, + disabled, + value = 5, +}) => { + const [rangeValue, setRangeValue] = useState(value); + + function handleChange(e: React.ChangeEvent) { + const newValue = parseInt(e.target.value); // Parse the value to an integer + setRangeValue(newValue); // Update the local state + + if (onChange) { + onChange(newValue); // Call the onChange function if it exists + } + } + useEffect(() => { + setRangeValue(value); + }, [value]); + + return ( +
+ +
+ + +
+
+ ); +}; + +export default InputRange; diff --git a/app/src/components/ui/inputs/InputToggle.tsx b/app/src/components/ui/inputs/InputToggle.tsx new file mode 100644 index 0000000..0774100 --- /dev/null +++ b/app/src/components/ui/inputs/InputToggle.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, useState } from "react"; + +interface InputToggleProps { + label: string; // Represents the toggle state (on/off) + onClick?: () => void; // Function to handle toggle clicks + value?: boolean; + inputKey: string; +} + +const InputToggle: React.FC = ({ + label, + onClick, + value = false, + inputKey, +}) => { + const [activeValue, setActiveValue] = useState(value); + + function handleOnClick() { + setActiveValue(!activeValue); + if (onClick) onClick(); + } + + useEffect(() => { + setActiveValue(value); + }, [value]); + + return ( +
+ +
+
+ +
+
+ ); +}; + +export default InputToggle; diff --git a/app/src/components/ui/inputs/InputWithDropDown.tsx b/app/src/components/ui/inputs/InputWithDropDown.tsx index ca51112..99453d7 100644 --- a/app/src/components/ui/inputs/InputWithDropDown.tsx +++ b/app/src/components/ui/inputs/InputWithDropDown.tsx @@ -1,10 +1,14 @@ import React, { useState } from "react"; +import RenameInput from "./RenameInput"; type InputWithDropDownProps = { label: string; value: string; options?: string[]; // Array of dropdown options activeOption?: string; // The currently active dropdown option + onClick?: () => void; + onChange: (newValue: string) => void; + editableLabel?: boolean; }; const InputWithDropDown: React.FC = ({ @@ -12,6 +16,9 @@ const InputWithDropDown: React.FC = ({ value, options, activeOption, + onClick, + onChange, + editableLabel = false, }) => { const separatedWords = label .split(/(?=[A-Z])/) @@ -22,29 +29,45 @@ const InputWithDropDown: React.FC = ({ return (
- + {editableLabel ? ( + + ) : ( + + )}
- - -
{ - setOpenDropdown(true); + { + onChange(e.target.value); }} - > -
{activeOption}
- {options && openDropdown && ( -
- {options.map((option, index) => ( -
- {option} -
- ))} -
- )} -
+ /> + + {activeOption && ( +
{ + setOpenDropdown(true); + }} + > +
{activeOption}
+ {options && openDropdown && ( +
+ {options.map((option, index) => ( +
+ {option} +
+ ))} +
+ )} +
+ )}
); diff --git a/app/src/components/ui/inputs/LabledButton.tsx b/app/src/components/ui/inputs/LabledButton.tsx new file mode 100644 index 0000000..26f4d75 --- /dev/null +++ b/app/src/components/ui/inputs/LabledButton.tsx @@ -0,0 +1,26 @@ +import React from "react"; + +interface LabeledButtonProps { + label: string; // Label for the button + onClick?: () => void; // Function to call when the button is clicked + disabled?: boolean; // Optional prop to disable the button + value?: string; +} + +const LabeledButton: React.FC = ({ + label, + onClick, + disabled = false, + value = "Click here", +}) => { + return ( +
+
{label}
+ +
+ ); +}; + +export default LabeledButton; diff --git a/app/src/components/ui/inputs/LabledDropdown.tsx b/app/src/components/ui/inputs/LabledDropdown.tsx index 1d35d85..197d5f1 100644 --- a/app/src/components/ui/inputs/LabledDropdown.tsx +++ b/app/src/components/ui/inputs/LabledDropdown.tsx @@ -20,6 +20,7 @@ const LabledDropdown: React.FC = ({ defaultOption, options header={activeOption} // Display the current active option options={options} // Use the options from props onSelect={handleSelect} // Handle option selection + search = {false} />
); diff --git a/app/src/components/ui/inputs/MultiEmailInvite.tsx b/app/src/components/ui/inputs/MultiEmailInvite.tsx new file mode 100644 index 0000000..7cfa1a0 --- /dev/null +++ b/app/src/components/ui/inputs/MultiEmailInvite.tsx @@ -0,0 +1,71 @@ +import React, { useState } from "react"; + +const MultiEmailInvite: React.FC = () => { + const [emails, setEmails] = useState([]); + const [inputValue, setInputValue] = useState(""); + + const handleAddEmail = () => { + const trimmedEmail = inputValue.trim(); + + // Validate email + if (!trimmedEmail || !validateEmail(trimmedEmail)) { + alert("Please enter a valid email address."); + return; + } + + // Check for duplicates + if (emails.includes(trimmedEmail)) { + alert("This email has already been added."); + return; + } + + // Add email to the list + setEmails([...emails, trimmedEmail]); + setInputValue(""); // Clear the input field after adding + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" || e.key === ",") { + e.preventDefault(); + handleAddEmail(); + } + }; + + const handleRemoveEmail = (emailToRemove: string) => { + setEmails(emails.filter((email) => email !== emailToRemove)); + }; + + const validateEmail = (email: string) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const [inputFocus, setInputFocus] = useState(false); + + return ( +
+
+ {emails.map((email, index) => ( +
+ {email} + handleRemoveEmail(email)}>× +
+ ))} + setInputValue(e.target.value)} + onFocus={() => setInputFocus(true)} + onBlur={() => setInputFocus(false)} + onKeyDown={handleKeyDown} + placeholder="Enter email and press Enter or comma" + /> +
+
+ Invite +
+
+ ); +}; + +export default MultiEmailInvite; diff --git a/app/src/components/ui/inputs/RegularDropDown.tsx b/app/src/components/ui/inputs/RegularDropDown.tsx index 318913f..f08cc46 100644 --- a/app/src/components/ui/inputs/RegularDropDown.tsx +++ b/app/src/components/ui/inputs/RegularDropDown.tsx @@ -4,53 +4,81 @@ interface DropdownProps { header: string; options: string[]; onSelect: (option: string) => void; + search?: boolean; + onClick?: () => void; + onChange?: () => void; } const RegularDropDown: React.FC = ({ header, options, onSelect, + search = true, + onClick, + onChange, }) => { const [isOpen, setIsOpen] = useState(false); const [selectedOption, setSelectedOption] = useState(null); - const dropdownRef = useRef(null); // Create a ref for the dropdown container + const [searchTerm, setSearchTerm] = useState(""); // State to store search term + const [filteredOptions, setFilteredOptions] = useState(options); // State for filtered options + const dropdownRef = useRef(null); // Ref for the dropdown container // Reset selectedOption when the dropdown closes useEffect(() => { if (!isOpen) { - setSelectedOption(null); // Clear local state when the dropdown closes + setSelectedOption(null); + setSearchTerm(""); // Clear the search term when the dropdown closes + setFilteredOptions(options); // Reset filtered options when the dropdown closes } - }, [isOpen]); + }, [isOpen, options]); // Reset selectedOption when the header prop changes useEffect(() => { - setSelectedOption(null); // Ensure the dropdown reflects the updated header - }, [header]); + setSelectedOption(null); + setSearchTerm(""); // Reset search term if header changes + setFilteredOptions(options); // Reset options if header changes + }, [header, options]); // Close dropdown if clicked outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); // Close the dropdown if clicked outside + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); } }; document.addEventListener("click", handleClickOutside); - // Cleanup the event listener on component unmount return () => { document.removeEventListener("click", handleClickOutside); }; }, []); + // Toggle the dropdown const toggleDropdown = () => { setIsOpen((prev) => !prev); }; + // Handle option selection const handleOptionClick = (option: string) => { setSelectedOption(option); - onSelect(option); // Call the onSelect function passed from the parent - setIsOpen(false); // Close the dropdown after selection + onSelect(option); + setIsOpen(false); + }; + + // Handle search input change + const handleSearchChange = (event: React.ChangeEvent) => { + const term = event.target.value; + setSearchTerm(term); + + // Filter options based on the search term + const filtered = options.filter((option) => + option.toLowerCase().includes(term.toLowerCase()) + ); + setFilteredOptions(filtered); }; return ( @@ -64,15 +92,32 @@ const RegularDropDown: React.FC = ({ {/* Dropdown Options */} {isOpen && (
- {options.map((option, index) => ( -
handleOptionClick(option)} - > - {option} + {/* Search Bar */} + {search && ( +
+
- ))} + )} + + {/* Filtered Options */} + {filteredOptions.length > 0 ? ( + filteredOptions.map((option, index) => ( +
handleOptionClick(option)} + > + {option} +
+ )) + ) : ( +
No options found
+ )}
)}
diff --git a/app/src/components/ui/list/DropDownList.tsx b/app/src/components/ui/list/DropDownList.tsx index 093765c..88386f8 100644 --- a/app/src/components/ui/list/DropDownList.tsx +++ b/app/src/components/ui/list/DropDownList.tsx @@ -81,12 +81,6 @@ const DropDownList: React.FC = ({ showAddIcon={false} items={[]} /> - )}
diff --git a/app/src/functions/idbUtils.ts b/app/src/functions/idbUtils.ts new file mode 100644 index 0000000..bc7c30d --- /dev/null +++ b/app/src/functions/idbUtils.ts @@ -0,0 +1,45 @@ +const DB_NAME = 'GLTFStorage'; +const STORE_NAME = 'models'; +const DB_VERSION = 1; + +export function initializeDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME); + } + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +export async function storeGLTF(key: string, file: Blob): Promise { + const db = await initializeDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + const request = store.put(file, key); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +export async function retrieveGLTF(key: string): Promise { + const db = await initializeDB(); + + return new Promise((resolve, reject) => { + const transaction = db.transaction(STORE_NAME, 'readonly'); + const store = transaction.objectStore(STORE_NAME); + const request = store.get(key); + + request.onsuccess = () => resolve(request.result as Blob | undefined); + request.onerror = () => reject(request.error); + }); +} \ No newline at end of file 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..43f8af4 --- /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 '../world/worldConstants'; +import DragPoint from '../geomentries/points/dragPoint'; + +import * as Types from "../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..ebf7edd --- /dev/null +++ b/app/src/modules/builder/eventFunctions/handleContextMenu.ts @@ -0,0 +1,8 @@ +import * as Types from "../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..e952412 --- /dev/null +++ b/app/src/modules/builder/eventFunctions/handleMeshDown.ts @@ -0,0 +1,64 @@ +import * as THREE from 'three'; + +import * as Types from "../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..4d8af8c --- /dev/null +++ b/app/src/modules/builder/eventFunctions/handleMeshMissed.ts @@ -0,0 +1,34 @@ +import * as THREE from 'three'; +import * as Types from "../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..1194a25 --- /dev/null +++ b/app/src/modules/builder/functions/deletableLineOrPoint.ts @@ -0,0 +1,87 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../world/worldConstants'; +import * as Types from "../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..bd5379b --- /dev/null +++ b/app/src/modules/builder/functions/draw.ts @@ -0,0 +1,97 @@ +import * as Types from "../world/worldTypes"; +import * as CONSTANTS from '../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..5ff54ca --- /dev/null +++ b/app/src/modules/builder/geomentries/aisles/addAilseToScene.ts @@ -0,0 +1,56 @@ +import * as THREE from 'three'; +import * as Types from '../../world/worldTypes'; +import * as CONSTANTS from '../../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..ad976b3 --- /dev/null +++ b/app/src/modules/builder/geomentries/aisles/loadAisles.ts @@ -0,0 +1,19 @@ +import * as Types from '../../world/worldTypes'; +import addAisleToScene from './addAilseToScene'; +import * as CONSTANTS from '../../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..eef3c40 --- /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 "../../world/worldTypes"; +import { retrieveGLTF, storeGLTF } from '../../indexDB/idbUtils'; +import { setFloorItemApi } from '../../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi'; +import { Socket } from 'socket.io-client'; +import * as CONSTANTS from '../../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..f9d5aa9 --- /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 "../../world/worldTypes"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; +import { initializeDB, retrieveGLTF, storeGLTF } from "../../indexDB/idbUtils"; +import * as CONSTANTS from '../../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..db0aa19 --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/assetVisibility.ts @@ -0,0 +1,25 @@ +import * as Types from "../../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..866c37d --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/deletableHoveredFloorItems.ts @@ -0,0 +1,43 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..53d488f --- /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 "../../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..db0f976 --- /dev/null +++ b/app/src/modules/builder/geomentries/assets/tempLoader.ts @@ -0,0 +1,29 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..57e5816 --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/addFloorToScene.ts @@ -0,0 +1,64 @@ +import * as THREE from 'three'; +import * as Types from "../../world/worldTypes"; +import * as CONSTANTS from "../../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..7b5cdaf --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/drawOnlyFloor.ts @@ -0,0 +1,179 @@ +import * as THREE from 'three'; + +import * as Types from "../../world/worldTypes"; +import * as CONSTANTS from '../../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..b8549e1 --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/loadFloor.ts @@ -0,0 +1,50 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../world/worldConstants'; +import addRoofToScene from '../roofs/addRoofToScene'; + +import * as Types from "../../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..f458cb5 --- /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 '../../world/worldConstants'; +import * as Types from "../../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..db1a990 --- /dev/null +++ b/app/src/modules/builder/geomentries/floors/updateFloorLines.ts @@ -0,0 +1,24 @@ +import * as Types from "../../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..3c6c58d --- /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 '../../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..f0959fc --- /dev/null +++ b/app/src/modules/builder/geomentries/layers/layer2DVisibility.ts @@ -0,0 +1,35 @@ +import * as Types from "../../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..d1de51f --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/addLineToScene.ts @@ -0,0 +1,24 @@ +import * as THREE from "three"; +import * as CONSTANTS from '../../world/worldConstants'; +import * as Types from "../../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..9ec93e4 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/createAndMoveReferenceLine.ts @@ -0,0 +1,98 @@ +import * as THREE from "three"; +import * as CONSTANTS from '../../world/worldConstants'; +import * as Types from "../../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..bf5a99d --- /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 "../../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/drawWall.ts b/app/src/modules/builder/geomentries/lines/drawWall.ts new file mode 100644 index 0000000..2cf4d32 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/drawWall.ts @@ -0,0 +1,167 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../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 "../../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..b347f34 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/getClosestIntersection.ts @@ -0,0 +1,26 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..ad59847 --- /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 '../../world/worldConstants'; +import * as Types from "../../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..343c5ae --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLineToObject.ts @@ -0,0 +1,24 @@ +import * as Types from "../../../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..788a6d7 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/lineConvertions/arrayLinesToObject.ts @@ -0,0 +1,30 @@ +import * as Types from "../../../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/removeConnectedLines.ts b/app/src/modules/builder/geomentries/lines/removeConnectedLines.ts new file mode 100644 index 0000000..bc85161 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/removeConnectedLines.ts @@ -0,0 +1,66 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..1c3fc22 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/removeReferenceLine.ts @@ -0,0 +1,22 @@ +import * as Types from "../../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..77f3c97 --- /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 "../../world/worldTypes"; +import { deleteLineApi } from '../../../../services/factoryBuilder/lines/deleteLineApi'; +import arrayLineToObject from '../lines/lineConvertions/arrayLineToObject'; +import { setLine } from '../../../../services/factoryBuilder/lines/setLineApi'; +import { Socket } from 'socket.io-client'; + +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/updateLines.ts b/app/src/modules/builder/geomentries/lines/updateLines.ts new file mode 100644 index 0000000..14b1762 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/updateLines.ts @@ -0,0 +1,24 @@ +import * as THREE from 'three'; +import * as Types from "../../world/worldTypes"; +import * as CONSTANTS from '../../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..6b2fabd --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/updateLinesPositions.ts @@ -0,0 +1,32 @@ +import * as Types from "../../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..adaadd7 --- /dev/null +++ b/app/src/modules/builder/geomentries/lines/vectorizeLinesCurrent.ts @@ -0,0 +1,18 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..272c793 --- /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 "../../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..5b1cb9a --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/addPillar.ts @@ -0,0 +1,24 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../world/worldConstants'; +import * as Types from "../../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..493f36e --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/deletableHoveredPillar.ts @@ -0,0 +1,34 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..9d20c92 --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/deletePillar.ts @@ -0,0 +1,21 @@ +import { toast } from 'react-toastify'; + +import * as Types from "../../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..3e7d2ce --- /dev/null +++ b/app/src/modules/builder/geomentries/pillars/updateReferencePolesheight.ts @@ -0,0 +1,40 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..6cf88e8 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/addPointToScene.ts @@ -0,0 +1,65 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../world/worldConstants'; +import * as Types from "../../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..cc93dd4 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/deletePoint.ts @@ -0,0 +1,57 @@ +import * as Types from "../../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..7e459a6 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/dragPoint.ts @@ -0,0 +1,44 @@ +import * as THREE from "three"; +import * as Types from "../../world/worldTypes"; +import * as CONSTANTS from '../../world/worldConstants'; + +import updateLinesPositions from "../lines/updateLinesPositions"; +import updateLines from "../lines/updateLines"; +import updateDistanceText from "../../../3d-ui/functions/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..85353d8 --- /dev/null +++ b/app/src/modules/builder/geomentries/points/removeSoloPoint.ts @@ -0,0 +1,37 @@ +import * as Types from "../../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..cb1f9bf --- /dev/null +++ b/app/src/modules/builder/geomentries/roofs/addRoofToScene.ts @@ -0,0 +1,32 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../../world/worldConstants'; +import * as Types from "../../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..74387fe --- /dev/null +++ b/app/src/modules/builder/geomentries/roofs/hideRoof.ts @@ -0,0 +1,47 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..6339f62 --- /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 "../../world/worldTypes"; +import * as CONSTANTS from '../../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..082354e --- /dev/null +++ b/app/src/modules/builder/geomentries/walls/deleteWallItems.ts @@ -0,0 +1,59 @@ +import { toast } from 'react-toastify'; + +import * as Types from "../../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..7e9cd15 --- /dev/null +++ b/app/src/modules/builder/geomentries/walls/hideWalls.ts @@ -0,0 +1,45 @@ +import * as THREE from 'three'; + +import * as Types from "../../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..a0093ba --- /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 '../../world/worldConstants'; + +import * as Types from "../../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..87258ec --- /dev/null +++ b/app/src/modules/builder/geomentries/zones/addZonesToScene.ts @@ -0,0 +1,50 @@ +import * as THREE from 'three'; +import * as Types from '../../world/worldTypes'; +import * as CONSTANTS from '../../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..fa3b52b --- /dev/null +++ b/app/src/modules/builder/geomentries/zones/loadZones.ts @@ -0,0 +1,19 @@ +import * as Types from '../../world/worldTypes'; +import * as THREE from 'three'; +import * as CONSTANTS from '../../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..bd1ff0f --- /dev/null +++ b/app/src/modules/builder/groups/floorGroupAisle.tsx @@ -0,0 +1,245 @@ +import * as THREE from 'three'; +import * as Types from '../world/worldTypes'; +import * as CONSTANTS from '../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..471f33b --- /dev/null +++ b/app/src/modules/builder/groups/floorItemsGroup.tsx @@ -0,0 +1,279 @@ +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 "../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 "../IntialLoad/loadInitialFloorItems"; +import addAssetModel from "../geomentries/assets/addAssetModel"; +import { getFloorItems } from "../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi"; +import { retrieveGLTF } from "../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) { + 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..97c7beb --- /dev/null +++ b/app/src/modules/builder/groups/floorPlanGroup.tsx @@ -0,0 +1,197 @@ +import { useEffect } from "react"; +import * as Types from '../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 "../IntialLoad/loadInitialPoint"; +import loadInitialLine from "../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..05a1e86 --- /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 "../world/worldTypes"; +import * as CONSTANTS from "../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 "../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..aa3fb38 --- /dev/null +++ b/app/src/modules/builder/groups/wallsMesh.tsx @@ -0,0 +1,65 @@ +import * as THREE from 'three'; +import * as Types from '../world/worldTypes'; +import * as CONSTANTS from '../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..14b0f6e --- /dev/null +++ b/app/src/modules/builder/groups/zoneGroup.tsx @@ -0,0 +1,467 @@ +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, useUpdateScene } 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 '../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(() => { + + const zonesWithVectors = zones.map((zone: any) => (zone.points.map((point: [number, number, number]) => new THREE.Vector3(...point)))); + + + }, [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..d50d8a6 --- /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 '../world/worldTypes'; +import * as CONSTANTS from "../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/builder/measurementTools/measurementTool.tsx b/app/src/modules/builder/measurementTools/measurementTool.tsx new file mode 100644 index 0000000..3e2a5a7 --- /dev/null +++ b/app/src/modules/builder/measurementTools/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/collaboration/collabCams.tsx b/app/src/modules/collaboration/collabCams.tsx new file mode 100644 index 0000000..a42d49a --- /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/models/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/images/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..e151523 --- /dev/null +++ b/app/src/modules/collaboration/socketResponses.dev.tsx @@ -0,0 +1,778 @@ +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 "../world/worldTypes"; +import * as CONSTANTS from '../world/worldConstants'; +import TempLoader from "../geomentries/assets/tempLoader"; + +import { setFloorItemApi } from "../../../services/factoryBuilder/assest/floorAsset/setFloorItemApi"; +import objectLineToArray from "../geomentries/lines/lineConvertions/objectLineToArray"; +import addLineToScene from "../geomentries/lines/addLineToScene"; +import updateLinesPositions from "../geomentries/lines/updateLinesPositions"; +import updateLines from "../geomentries/lines/updateLines"; +import updateDistanceText from "../../3d-ui/functions/updateDistanceText"; +import updateFloorLines from "../geomentries/floors/updateFloorLines"; +import loadWalls from "../geomentries/walls/loadWalls"; +import deletePoint from "../geomentries/points/deletePoint"; +import RemoveConnectedLines from "../geomentries/lines/removeConnectedLines"; +import Layer2DVisibility from "../geomentries/layers/layer2DVisibility"; +import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; +import { retrieveGLTF, storeGLTF } from "../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..cb65d4b --- /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 '../world/worldConstants'; +import { toast } from 'react-toastify'; +import * as Types from "../world/worldTypes"; +import { getFloorItems } from '../../../services/factoryBuilder/assest/floorAsset/getFloorItemsApi'; +import { initializeDB, retrieveGLTF, storeGLTF } from '../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..4cc74b5 --- /dev/null +++ b/app/src/modules/scene/IntialLoad/loadInitialLine.ts @@ -0,0 +1,30 @@ +import addLineToScene from "../geomentries/lines/addLineToScene"; +import * as CONSTANTS from '../world/worldConstants'; +import * as Types from "../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..777f3a8 --- /dev/null +++ b/app/src/modules/scene/IntialLoad/loadInitialPoint.ts @@ -0,0 +1,87 @@ +import * as THREE from 'three'; + +import * as CONSTANTS from '../world/worldConstants'; +import * as Types from "../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..e3624bd --- /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 "../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..54c8371 --- /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 '../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..c76a33f --- /dev/null +++ b/app/src/modules/scene/camera/switchToFirstPerson.ts @@ -0,0 +1,25 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../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..d5acf2f --- /dev/null +++ b/app/src/modules/scene/camera/switchToThirdPerson.ts @@ -0,0 +1,29 @@ +import * as THREE from 'three'; +import * as CONSTANTS from '../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..9611992 --- /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 '../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..cb024a0 --- /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 '../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..275aff9 --- /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 "../../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..296aa4a --- /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 "../../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..822e7e5 --- /dev/null +++ b/app/src/modules/scene/controls/selection/moveControls.tsx @@ -0,0 +1,210 @@ +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 "../../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]); + + useFrame(() => { + if (movedObjects.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(); + movedObjects.forEach((obj: THREE.Object3D) => box.expandByObject(obj)); + 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 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..06665a6 --- /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 "../../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..c535c43 --- /dev/null +++ b/app/src/modules/scene/controls/selection/selectionControls.tsx @@ -0,0 +1,225 @@ +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 "../../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; + + const helper = new SelectionHelper(gl); + + if (!itemsGroup) { + toast.warn("itemsGroup not found in the scene."); + return; + } + + const onPointerDown = (event: PointerEvent) => { + isSelecting = false; + 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) { + selectionBox.endPoint.set(pointer.x, pointer.y, 0); + } + }; + + const onPointerUp = (event: PointerEvent) => { + if (isSelecting) { + 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; + } + }; + + 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(); + } + }; + + if (!toggleView) { + helper.enabled = true; + 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); + 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..181646c --- /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 '../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..726579d --- /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 '../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..ef4c4a0 --- /dev/null +++ b/app/src/modules/scene/environment/shadow.tsx @@ -0,0 +1,84 @@ +import { useRef, useEffect, useState } 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 '../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..77f56f3 --- /dev/null +++ b/app/src/modules/scene/environment/sky.tsx @@ -0,0 +1,50 @@ +import * as THREE from 'three'; +import * as Types from '../world/worldTypes'; +import { Sky } from "@react-three/drei"; +import { useAzimuth, useElevation, useSunPosition } from "../../../store/store"; +import { useEffect, useRef, useState } from "react"; +import * as CONSTANTS from '../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..44f326c --- /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 "../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..58cb671 --- /dev/null +++ b/app/src/modules/scene/postProcessing/postProcessing.tsx @@ -0,0 +1,79 @@ +import * as THREE from 'three' +import { EffectComposer, N8AO, Outline } from '@react-three/postprocessing' +import { BlendFunction } from 'postprocessing' +import { useDeletableFloorItem, useSelectedWallItem, useselectedFloorItem } from '../../../store/store'; +import * as Types from '../world/worldTypes' +import * as CONSTANTS from '../world/worldConstants'; + +export default function PostProcessing() { + const { deletableFloorItem, setDeletableFloorItem } = useDeletableFloorItem(); + const { selectedWallItem, setSelectedWallItem } = useSelectedWallItem(); + const { selectedFloorItem, setselectedFloorItem } = useselectedFloorItem(); + + 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 && + + } + + + ) +} \ 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..197c9fe --- /dev/null +++ b/app/src/modules/scene/scene.tsx @@ -0,0 +1,57 @@ +import { useMemo } from "react"; +import * as THREE from "three"; +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 "./collab/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..07a858d --- /dev/null +++ b/app/src/modules/scene/world/world.tsx @@ -0,0 +1,333 @@ +////////// 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 ////////// + + +////////// Assests Imports ////////// + +import arch from "../../../assets/models/gltf-glb/arch.glb"; +import door from "../../../assets/models/gltf-glb/door.glb"; +import Window from "../../../assets/models/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 "../geomentries/walls/loadWalls"; + +import * as Types from "./worldTypes"; + +import SocketResponses from "../collab/socketResponses.dev"; +import FloorItemsGroup from "../groups/floorItemsGroup"; +import FloorPlanGroup from "../groups/floorPlanGroup"; +import FloorGroup from "../groups/floorGroup"; +import FloorGroupAilse from "../groups/floorGroupAisle"; +import Draw from "../functions/draw"; +import WallsAndWallItems from "../groups/wallsAndWallItems"; +import Ground from "../environment/ground"; +// import ZoneGroup from "../groups/zoneGroup1"; +import { findEnvironment } from "../../../services/factoryBuilder/environment/findEnvironment"; +import Layer2DVisibility from "../geomentries/layers/layer2DVisibility"; +import ZoneGroup from "../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/scene/world/worldConstants.ts b/app/src/modules/scene/world/worldConstants.ts new file mode 100644 index 0000000..ce5cacd --- /dev/null +++ b/app/src/modules/scene/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/modules/scene/world/worldTypes.d.ts b/app/src/modules/scene/world/worldTypes.d.ts new file mode 100644 index 0000000..32293d9 --- /dev/null +++ b/app/src/modules/scene/world/worldTypes.d.ts @@ -0,0 +1,271 @@ +// 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; \ No newline at end of file diff --git a/app/src/modules/simulation/simulation/behaviour/behaviour.tsx b/app/src/modules/simulation/simulation/behaviour/behaviour.tsx new file mode 100644 index 0000000..f87e566 --- /dev/null +++ b/app/src/modules/simulation/simulation/behaviour/behaviour.tsx @@ -0,0 +1,85 @@ +import { useFloorItems, useRenderDistance } from '../../../../store/store'; +import * as THREE from 'three'; +import * as Types from '../../world/worldTypes'; +import { useFrame, useThree } from '@react-three/fiber'; +import { useEffect, useRef } from 'react'; + +interface Path { + modeluuid: string; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + events: string; + triggers: string; + }[]; + position: [number, number, number]; +} + +function Behaviour({ setPaths }: { setPaths: any }) { + const { floorItems } = useFloorItems(); + + useEffect(() => { + const newPaths: Path[] = []; + + floorItems.forEach((item: Types.FloorItemType) => { + if (item.modelfileID === "6633215057b31fe671145959") { + const rotationY = item.rotation.y; + const offsetX = Math.cos(rotationY) * 3.3; + const offsetZ = Math.sin(rotationY) * 3.3; + const positioY = 1.25; + + const point1Position = new THREE.Vector3(offsetX, positioY, offsetZ); + const point2Position = new THREE.Vector3(-offsetX, positioY, -offsetZ); + const middlePointPosition = new THREE.Vector3().addVectors(point1Position, point2Position).multiplyScalar(0.5); + + 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: 'null', + triggers: 'null', + }, + { + uuid: middlePointUUID, + position: [middlePointPosition.x, middlePointPosition.y, middlePointPosition.z], + rotation: [0, 0, 0], + events: 'null', + triggers: 'null', + }, + { + uuid: point2UUID, + position: [point2Position.x, point2Position.y, point2Position.z], + rotation: [0, 0, 0], + events: 'null', + triggers: 'null', + }, + ], + position: [...item.position], + }; + + newPaths.push(newPath); + } + }); + + setPaths((prevPaths: Path[]) => [...prevPaths, ...newPaths]); + + return () => { + setPaths([]); + }; + }, [floorItems, setPaths]); + + return ( + <> + + ); +} + +export default Behaviour; \ No newline at end of file diff --git a/app/src/modules/simulation/simulation/path/pathConnector.tsx b/app/src/modules/simulation/simulation/path/pathConnector.tsx new file mode 100644 index 0000000..fe17d5d --- /dev/null +++ b/app/src/modules/simulation/simulation/path/pathConnector.tsx @@ -0,0 +1,70 @@ +import { useThree } from '@react-three/fiber'; +import React, { useEffect } from 'react' +import * as THREE from 'three'; +import { useActiveMenu } from '../../../../store/store'; + +function PathConnector({ pathsGroupRef }: { pathsGroupRef: React.MutableRefObject }) { + const { activeMenu } = useActiveMenu(); + const { gl, raycaster, scene, pointer, camera } = useThree(); + + 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); + + let intersects = raycaster.intersectObjects(pathsGroupRef.current.children, true); + if (intersects.length > 0 && intersects[0].object.name.includes("event-sphere")) { + const intersectedSphere = intersects[0]; + console.log('intersectedSphere: ', intersectedSphere); + } + + }; + + const onMouseClick = (evt: MouseEvent) => { + + }; + + if (activeMenu === 'Simulation') { + 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); + }; + }, [camera, scene, raycaster]); + + return ( + <> + ) +} + +export default PathConnector \ No newline at end of file diff --git a/app/src/modules/simulation/simulation/path/pathCreation.tsx b/app/src/modules/simulation/simulation/path/pathCreation.tsx new file mode 100644 index 0000000..a1a55a6 --- /dev/null +++ b/app/src/modules/simulation/simulation/path/pathCreation.tsx @@ -0,0 +1,78 @@ +import * as THREE from 'three'; +import { useRef } from 'react'; +import { Sphere } from '@react-three/drei'; +import { useRenderDistance, useSelectedEventSphere } 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: string; + triggers: string; + }[]; + position: [number, number, number]; +} + +function PathCreation({ pathsGroupRef, paths }: { pathsGroupRef: React.MutableRefObject, paths: Path[] }) { + const { renderDistance } = useRenderDistance(); + const { SelectedEventSphere } = useSelectedEventSphere(); + const { camera } = useThree(); + const groupRefs = useRef<{ [key: string]: THREE.Group }>({}); + + useFrame(() => { + Object.values(groupRefs.current).forEach(group => { + if (group) { + const distance = new THREE.Vector3(...group.position.toArray()).distanceTo(camera.position); + group.visible = distance <= renderDistance; + } + }); + }); + + return ( + + {paths.map((path: Path) => { + const points = path.points.map(point => new THREE.Vector3(...point.position)); + + return ( + (groupRefs.current[path.modeluuid] = el!)} + position={path.position} + rotation={[0, -Math.PI / 2, 0]} + > + {path.points.map((point, index) => ( + + + + ))} + + {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 ( + + + + ); + })} + + ); + })} + + ) +} + +export default PathCreation \ No newline at end of file diff --git a/app/src/modules/simulation/simulation/simulation.tsx b/app/src/modules/simulation/simulation/simulation.tsx new file mode 100644 index 0000000..f202b19 --- /dev/null +++ b/app/src/modules/simulation/simulation/simulation.tsx @@ -0,0 +1,47 @@ +import { useState, useEffect, useRef } from 'react'; +import { useActiveMenu, useFloorItems } 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'; + +interface Path { + modeluuid: string; + points: { + uuid: string; + position: [number, number, number]; + rotation: [number, number, number]; + events: string; + triggers: string; + }[]; + position: [number, number, number]; +} + +function Simulation() { + const { activeMenu } = useActiveMenu(); + const { floorItems } = useFloorItems(); + const { scene } = useThree(); + const pathsGroupRef = useRef() as React.MutableRefObject; + const [paths, setPaths] = useState([]); + const [connections, setConnections] = useState([]); + const [processes, setProcesses] = useState([]); + + useEffect(() => { + // console.log('paths: ', paths); + }, [paths]); + + return ( + <> + {activeMenu === '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..2492b1f --- /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 = { + paths: PathPoint[][]; + setPaths: React.Dispatch>; + connections: { start: PathPoint; end: PathPoint }[]; + setConnections: React.Dispatch> +}; + +const PathCreator = ({ paths, setPaths, 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) { + setPaths((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) { + setPaths((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 = paths.map((path) => + path.map((p) => + p.uuid === selectedPoint.uuid ? { ...p, position: updatedPosition, rotation: updatedRotation } : p + ) + ); + setPaths(updatedPaths); + } + }; + + + const meshContext = (uuid: string) => { + const pathIndex = paths.findIndex(path => path.some(point => point.uuid === uuid)); + if (pathIndex === -1) return; + + const clickedPoint = paths[pathIndex].find(point => point.uuid === uuid); + if (!clickedPoint) return; + + const isStart = paths[pathIndex][0].uuid === uuid; + const isEnd = paths[pathIndex][paths[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 paths */} + {paths.map((path, pathIndex) => ( + + point.position)} + color="yellow" + lineWidth={5} + userData={{ isPathObject: true }} + /> + + ))} + + {/* Render finalized points */} + {paths.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..7c20a4e --- /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 [paths, setPaths] = useState<{ position: THREE.Vector3; rotation: THREE.Quaternion; uuid: string }[][]>([]); + const [connections, setConnections] = useState<{ start: PathPoint; end: PathPoint }[]>([]); + + return ( + <> + + {paths.map((path, index) => ( + + ))} + + ); +} + +export default Simulation; \ No newline at end of file diff --git a/app/src/pages/UserAuth.tsx b/app/src/pages/UserAuth.tsx index 475cc81..a55f489 100644 --- a/app/src/pages/UserAuth.tsx +++ b/app/src/pages/UserAuth.tsx @@ -1,8 +1,157 @@ -import React from 'react'; +import React, { useState, FormEvent } from "react"; +import { useNavigate } from "react-router-dom"; +import { LogoIconLarge } from "../components/icons/Logo"; +import { EyeIcon } from "../components/icons/ExportCommonIcons"; +import LoadingPage from "../components/templates/LoadingPage"; const UserAuth: React.FC = () => { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(""); + const [isSignIn, setIsSignIn] = useState(true); // Toggle between login and register + const [userName, setUserName] = useState(""); // Username for registration + + const navigate = useNavigate(); + + const handleLogin = (e: FormEvent) => { + e.preventDefault(); + // Dummy validation for "account not found" + if (email !== "user@example.com") { + setError("Account not found"); + } else { + setError(""); + console.log("Login Successful!"); + console.log("Email:", email); + console.log("Password:", password); + } + }; + + const handleRegister = (e: FormEvent) => { + e.preventDefault(); + // Dummy validation for registration + if (email && password && userName) { + setError(""); + console.log("Registration Successful!"); + console.log("Username:", userName); + console.log("Email:", email); + console.log("Password:", password); + setIsSignIn(true); + } else { + setError("Please fill all the fields!"); + } + }; + return ( -
UserAuth
+ <> + {/* */} +
+
+ +
+

Welcome to Dwinzo

+

+ {isSignIn ? ( + <> + Don’t have an account?{" "} + setIsSignIn(false)} + style={{ cursor: "pointer" }} + > + Register here! + + + ) : ( + <> + Already have an account?{" "} + setIsSignIn(true)} + style={{ cursor: "pointer" }} + > + Login here! + + + )} +

+ + + + {error &&
🛈 {error}
} + +
+ {!isSignIn && ( + setUserName(e.target.value)} + required + /> + )} + setEmail(e.target.value)} + required + /> +
+ setPassword(e.target.value)} + required + /> + +
+ {!isSignIn && ( +
+ +
+ I have read and agree to the terms of service +
+
+ )} + +
+

+ By signing up for, or logging into, an account, you agree to our{" "} + navigate("/privacy")} + style={{ cursor: "pointer" }} + > + privacy policy + {" "} + &{" "} + navigate("/terms")} + style={{ cursor: "pointer" }} + > + terms of service + {" "} + whether you read them or not. You can also find these terms on our + website. +

+
+ ); }; 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/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/services/signInSignUp/signInApi.ts b/app/src/services/signInSignUp/signInApi.ts new file mode 100644 index 0000000..be608c0 --- /dev/null +++ b/app/src/services/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/signInSignUp/signUpApi.ts b/app/src/services/signInSignUp/signUpApi.ts new file mode 100644 index 0000000..87ded2b --- /dev/null +++ b/app/src/services/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/store/store.ts b/app/src/store/store.ts new file mode 100644 index 0000000..2e58395 --- /dev/null +++ b/app/src/store/store.ts @@ -0,0 +1,293 @@ +import { create } from "zustand"; +import { io } from "socket.io-client"; +import * as THREE from "three"; + +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 useActiveMenu = create((set: any) => ({ + activeMenu: "Factory builder", + setActiveMenu: (x: any) => set(() => ({ activeMenu: x })), +})); + +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, + SelectedEventSphere: (x: any) => set({ selectedEventSphere: x }), +})); \ No newline at end of file diff --git a/app/src/styles/abstracts/variables.scss b/app/src/styles/abstracts/variables.scss index b5244a6..f36e2dd 100644 --- a/app/src/styles/abstracts/variables.scss +++ b/app/src/styles/abstracts/variables.scss @@ -36,6 +36,8 @@ $background-color: #fcfdfd; // Main background color $background-color-dark: #19191d; // Main background color for dark mode $background-color-secondary: #e1e0ff80; // Secondary background color $background-color-secondary-dark: #39394f99; // Secondary background color for dark mode +$background-color-gray: #f3f3f3; // Main background color +$background-color-gray-dark: #232323; // Main background color for dark mode // Border colors $border-color: #e0dfff; // Default border color @@ -76,16 +78,11 @@ $xxlarge: 1.5rem; // Double extra large text size (24px) $xxxlarge: 2rem; // Triple extra large text size (32px) // Font weights +$thin-weight: 300; // Regular font weight $regular-weight: 400; // Regular font weight $medium-weight: 500; // Medium font weight $bold-weight: 600; // Bold font weight -// ======================================================================== -// Spacing -// ======================================================================== - -// Line heights and letter spacings (to be added as needed) - // ======================================================================== // Z-Index Levels // ======================================================================== diff --git a/app/src/styles/base/base.scss b/app/src/styles/base/base.scss index d742fa5..dcbe0be 100644 --- a/app/src/styles/base/base.scss +++ b/app/src/styles/base/base.scss @@ -11,10 +11,12 @@ --primary-color: #{$background-color}; // Primary color for light theme --accent-color: #{$accent-color}; // Primary accent color for light theme --highlight-accent-color: #{$highlight-accent-color}; // Highlight color for light theme + --accent-gradient-color: #{$acent-gradient}; // Primary accent color for light theme // Background colors --background-color: #{$background-color}; // Main background color --background-color-secondary: #{$background-color-secondary}; // Secondary background color + --background-color-gray: #{$background-color-gray}; // Secondary background color // Border colors --border-color: #{$border-color}; // Border color for light theme @@ -43,10 +45,12 @@ --primary-color: #{$highlight-accent-color-dark}; --accent-color: #{$accent-color-dark}; // Primary accent color for dark theme --highlight-accent-color: #{$highlight-accent-color-dark}; // Highlight color for dark theme + --accent-gradient-color: #{$acent-gradient-dark}; // Primary accent color for light theme // Background colors --background-color: #{$background-color-dark}; // Main background color --background-color-secondary: #{$background-color-secondary-dark}; // Secondary background color + --background-color-gray: #{$background-color-gray-dark}; // Secondary background color // Border colors --border-color: #{$border-color-dark}; // Border color for dark theme diff --git a/app/src/styles/components/_regularDropDown.scss b/app/src/styles/components/_regularDropDown.scss deleted file mode 100644 index 86ac078..0000000 --- a/app/src/styles/components/_regularDropDown.scss +++ /dev/null @@ -1,54 +0,0 @@ -@use '../abstracts/variables.scss' as *; - - -.regularDropdown-container { - width: 104px; - height: 22px; - border: 1px solid var(--text-color); // Ensure $border-color is defined - border-radius: 6px; - position: relative; - cursor: pointer; - padding: 0 6px; - - .dropdown-header { - height: 100%; - display: flex; - justify-content: space-between; - cursor: pointer; - // padding: 5px; - border: 1px solid var(--primary-color); - border-radius: 6px; - background-color: var(--background-color); - - // .icon { - // padding-right: 6px; - // } - } - - .dropdown-options { - position: absolute; // Ensure dropdown options position correctly - width: 100%; // Ensure options width matches the header - background-color: var(--primary-color); // Optional: Background color - border: 1px solid #ccc; // Optional: Border styling - border-radius: 4px; // Optional: Border radius - z-index: 10; // Ensure dropdown appears above other elements - max-height: 200px; // Optional: Limit height - overflow-y: auto; // Optional: Enable scrolling if content exceeds height - left: 0; - top: 104%; - - .option { - padding: 5px; - cursor: pointer; - flex-direction: row !important; - - &:hover { - background-color: var(--primary-color); // Optional: Hover effect - } - } - } - - .icon { - height: auto; - } -} \ No newline at end of file diff --git a/app/src/styles/components/input.scss b/app/src/styles/components/input.scss index 2186efe..3020c7d 100644 --- a/app/src/styles/components/input.scss +++ b/app/src/styles/components/input.scss @@ -144,8 +144,464 @@ .project-dropdowm-container { position: relative; height: 32px; - .project-name{ + .project-name { line-height: 32px; height: 100%; } } + +.regularDropdown-container { + width: 100%; + min-width: 80px; + padding: 2px 4px; + border: 1px solid var(--border-color); + border-radius: 6px; + position: relative; + cursor: pointer; + .dropdown-header { + height: 100%; + display: flex; + justify-content: space-between; + cursor: pointer; + border: 1px solid var(--primary-color); + border-radius: 6px; + background-color: var(--background-color); + } + + .dropdown-options { + position: absolute; + width: 100%; + background-color: var(--primary-color); + border: 1px solid var(--border-color); + border-radius: #{$border-radius-small}; + z-index: 10; + max-height: 200px; + overflow-y: auto; + left: 0; + top: 110%; + padding: 4px; + .dropdown-search { + margin-bottom: 4px; + } + .option { + padding: 2px 4px; + cursor: pointer; + flex-direction: row !important; + border-radius: #{$border-radius-small}; + &:hover { + color: var(--accent-color); + background-color: var(--highlight-accent-color); + } + } + } + + .icon { + height: auto; + } +} + +.input.default { + width: 100%; + position: relative; + .dropdown { + top: 3px; + right: 3px; + position: absolute; + background: var(--highlight-accent-color); + border-radius: #{$border-radius-small}; + padding: 1px 4px; + .active-option { + color: var(--accent-color); + font-size: var(--font-size-small); + } + } +} + +input { + width: 100%; + padding: 2px 4px; + border-radius: #{$border-radius-small}; + border: 1px solid var(--border-color); + outline: none; + &:focus, + &:active { + border: 1px solid var(--accent-color); + } +} + +.eye-dropper-input-container { + display: flex; + align-items: center; + .label { + width: 40%; + } + .input-container { + width: 60%; + position: relative; + @include flex-center; + gap: 4px; + .eye-picker-button { + height: 24px; + min-width: 24px; + @include flex-center; + border-radius: #{$border-radius-small}; + background: var(--background-color-secondary); + cursor: pointer; + } + .active { + background: var(--accent-color); + } + } +} + +.multi-level-dropdown { + position: relative; + display: inline-block; + + .dropdown-button { + width: 100%; + background-color: var(--background-color) !important; + border: 1px solid var(--border-color) !important; + padding: 5px 10px; + + // font-size: 12px; + cursor: pointer; + border-radius: 5px; + transition: background-color 0.3s ease; + + &:hover { + background-color: #333333; + } + + &.open { + background-color: #333333; + } + } + + .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + background-color: #ffffff; + border: 1px solid #cccccc; + border-radius: 5px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + z-index: 1000; + min-width: 200px; + overflow: auto; + max-height: 600px; + + .dropdown-content { + display: flex; + flex-direction: column; + gap: 6px; + + .nested-dropdown { + // &:first-child{ + margin-left: 0; + // } + } + + padding: 10px; + } + + .dropdown-item { + display: block; + padding: 5px 10px; + text-decoration: none; + color: #000000; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s ease; + + &:hover { + background-color: #f0f0f0; + } + } + + .nested-dropdown { + margin-left: 20px; + + .dropdown-trigger { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 10px; + cursor: pointer; + font-size: 14px; + color: #000000; + transition: background-color 0.3s ease; + + &:hover { + background-color: #f0f0f0; + } + + &.open { + background-color: #e0e0e0; + } + + .icon { + font-size: 12px; + margin-left: 5px; + } + } + + .submenu { + margin-top: 5px; + padding-left: 20px; + border-left: 2px solid #cccccc; + display: flex; + flex-direction: column; + gap: 6px; + } + } + } +} + +.input-toggle-container { + padding: 8px 12px; + display: flex; + justify-content: space-between; + align-items: center; + + .label { + white-space: nowrap; + } + + .check-box { + height: 22px; + width: 44px; + background: var(--background-color-secondary); + border-radius: #{$border-radius-large}; + position: relative; + cursor: pointer; + + .check-box-style { + position: absolute; + height: 18px; + width: 18px; + top: 2px; + left: 2px; + background: var(--accent-color); + border-radius: #{$border-radius-circle}; + transition: left 0.3s ease; + } + + input { + display: none; + } + } +} + +.input-range-container { + @include flex-center; + padding: 6px 12px; + + .label { + width: 45%; + white-space: nowrap; + } + + .input-container { + @include flex-center; + width: 100%; + + input[type="range"] { + -webkit-appearance: none; + width: 100%; + height: 6px; + background: var(--background-color-secondary); + border-radius: #{$border-radius-medium}; + outline: none; + margin: 12px 8px; + cursor: pointer; + + &::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: var(--primary-color); + border-radius: #{$border-radius-circle}; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: transform 0.2s ease; + outline: 3px solid var(--accent-color); + outline-offset: -3px; + transform: translateY(-5px); + &:hover { + transform: scale(1.1) translateY(-5px); + } + } + + &::-moz-range-thumb { + width: 16px; + height: 16px; + background: var(--accent-color); + border: none; + border-radius: #{$border-radius-circle}; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.2); + } + } + + &::-ms-thumb { + width: 16px; + height: 16px; + background: var(--accent-color); + border: none; + border-radius: #{$border-radius-circle}; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: transform 0.2s ease; + + &:hover { + transform: scale(1.2); + } + } + + &::-webkit-slider-runnable-track { + height: 6px; + background: var(--background-color-secondary); + border-radius: #{$border-radius-medium}; + } + + &::-moz-range-track { + height: 6px; + background: var(--background-color-secondary); + border-radius: #{$border-radius-medium}; + } + + &::-ms-track { + height: 6px; + background: var(--background-color-secondary); + border-radius: #{$border-radius-medium}; + border: none; + } + + &::-ms-fill-lower, + &::-ms-fill-upper { + background: var(--background-color-secondary); + border-radius: #{$border-radius-medium}; + } + + &:disabled { + background: var(--text-disabled); + cursor: not-allowed; + + &::-webkit-slider-thumb { + background: var(--primary-color); + box-shadow: none; + outline: 4px solid var(--text-disabled); + outline: -4px; + } + + &::-moz-range-thumb { + background: var(--primary-color); + box-shadow: none; + } + + &::-ms-thumb { + background: var(--primary-color); + box-shadow: none; + } + + &::-webkit-slider-runnable-track, + &::-moz-range-track, + &::-ms-track, + &::-ms-fill-lower, + &::-ms-fill-upper { + background: var(--text-disabled); + } + } + } + + .input-value { + width: 40px; + text-align: center; + } + } +} + +.labeled-button-container { + @include flex-space-between; + padding: 6px 12px; + button { + padding: 2px 32px; + border: none; + border-radius: #{$border-radius-large}; + color: var(--text-disabled); + background: var(--accent-color); + transition: all 0.2s; + cursor: pointer; + &:hover { + color: var(--primary-color); + } + } +} + +.value-field-container { + margin-bottom: 6px; + padding: 6px 12px; + @include flex-space-between; + .label { + width: 40%; + } + .regularDropdown-container { + width: 60%; + } + .default { + width: 60%; + } +} + +.multi-email-invite-input-container { + @include flex-space-between; + gap: 20px; + .multi-email-invite-input { + width: 100%; + display: flex; + border: 1px solid var(--border-color); + padding: 2px 8px; + border-radius: #{$border-radius-large}; + flex-wrap: wrap; + max-height: 180px; + overflow: auto; + input { + border: none; + &::placeholder { + color: var(--text-disabled); + } + } + .entered-emails{ + @include flex-center; + gap: 2px; + background: var(--background-color-gray); + padding: 0 4px; + border-radius: #{$border-radius-large}; + span{ + height: 14px; + width: 14px; + line-height: 12px; + text-align: center; + border-radius: #{$border-radius-small}; + &:hover{ + background: var(--accent-color); + color: var(--primary-color); + } + } + } + } + .invite-button{ + padding: 4px 12px; + border-radius: #{$border-radius-large}; + background: var(--accent-color); + color: var(--primary-color); + } + .multi-email-invite-input.active { + border: 1px solid var(--accent-color); + } +} diff --git a/app/src/styles/layout/loading.scss b/app/src/styles/layout/loading.scss new file mode 100644 index 0000000..a6e3bf5 --- /dev/null +++ b/app/src/styles/layout/loading.scss @@ -0,0 +1,82 @@ +@use "../abstracts/variables" as *; +@use "../abstracts/mixins" as *; + +.loading-wrapper { + height: 100vh; + width: 100vw; + background: var(--background-color); + .loading-container { + position: relative; + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding: 28px; + z-index: 5; + &::after { + content: ""; + position: absolute; + background: radial-gradient( + circle, + #bfe0f8 0%, + #e9ebff 46%, + #e2acff 100% + ); + height: 50vh; + width: 50vw; + top: 0; + left: 0; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 50%; + filter: blur(200px); + z-index: -1; + } + .project-name { + font-size: var(--font-size-regular); + } + .loading-hero-container { + .logo { + @include flex-center; + width: 100%; + margin-bottom: 35px; + circle { + fill: transparent; + } + } + .content { + font-family: #{$font-josefin-sans}; + font-size: #{$xxlarge}; + font-weight: #{$thin-weight}; + max-width: 250px; + text-align: center; + line-height: 2rem; + } + } + .progress-container { + .progress-value { + font-family: #{$font-josefin-sans}; + font-weight: #{$thin-weight}; + font-size: 96px; + margin-bottom: 22px; + text-align: center; + } + .progress-indicator-container { + height: 6px; + width: 60vw; + background: var(--highlight-accent-color); + border-radius: #{$border-radius-small}; + position: relative; + .progress-bar { + height: 6px; + background: var(--accent-color); + border-radius: #{$border-radius-small}; + transition: width 0.2 ease; + } + } + } + } +} diff --git a/app/src/styles/layout/popup.scss b/app/src/styles/layout/popup.scss new file mode 100644 index 0000000..a086cf4 --- /dev/null +++ b/app/src/styles/layout/popup.scss @@ -0,0 +1,112 @@ +@use "../abstracts/variables" as *; +@use "../abstracts/mixins" as *; + +.collaboration-popup-wrapper { + height: 100vh; + width: 100vw; + background: var(--background-color-secondary); + backdrop-filter: blur(2px); + @include flex-center; + .collaboration-popup-container { + max-width: 50vw; + width: 460px; + background-color: var(--background-color); + border-radius: #{$border-radius-large}; + .split { + width: 100%; + height: 1px; + background: var(--highlight-accent-color); + } + .header { + @include flex-space-between; + padding: 12px; + border-bottom: 1px solid var(--border-color); + .content { + @include flex-center; + .copy-link-button { + font-size: var(--font-size-small); + &:hover { + color: var(--accent-color); + text-decoration: underline; + } + } + .close-button { + @include flex-center; + height: 20px; + width: 20px; + border-radius: #{$border-radius-small}; + &:hover { + background: var(--background-color-secondary); + } + svg { + scale: 1.5; + } + } + } + } + .invite-input-container { + padding: 12px; + } + .access-and-user-control-container { + padding: 12px; + .user-header { + margin-bottom: 12px; + } + .general-access-container, + .users-list-container { + .user-list-container, + .team-container, + .you-container { + @include flex-space-between; + padding: 8px 12px; + .user-details { + @include flex-center; + gap: 8px; + .profile-image { + height: 24px; + width: 24px; + line-height: 24px; + text-align: center; + border-radius: #{$border-radius-circle}; + overflow: hidden; + color: var(--primary-color); + img { + height: 100%; + width: 100%; + object-fit: cover; + } + .no-profile-container { + font-size: var(--font-size-small); + color: inherit; + } + } + .user-name { + font-size: var(--font-size-regulaar); + } + } + .project-name, + .your-name { + color: var(--accent-color); + } + .number-of-peoples-have-access { + padding: 4px 12px; + border-radius: #{$border-radius-small}; + } + .indicater { + padding: 2px 12px; + line-height: 22px; + border-radius: #{$border-radius-extra-large}; + background: var(--highlight-accent-color); + color: var(--accent-color); + outline: 1px dashed var(--accent-color); + outline-offset: -1px; + } + } + .team-container, + .you-container { + border-top: 1px solid var(--border-color); + } + } + } + } +} diff --git a/app/src/styles/layout/sidebar.scss b/app/src/styles/layout/sidebar.scss index 54ebf0d..b2c4d99 100644 --- a/app/src/styles/layout/sidebar.scss +++ b/app/src/styles/layout/sidebar.scss @@ -174,7 +174,7 @@ color: var(--primary-color); background-color: var(--accent-color); font-weight: var(--font-weight-regular); - border-radius: #{$border-radius-medium}; + border-radius: #{$border-radius-large}; cursor: pointer; } @@ -301,7 +301,6 @@ align-items: center; .multi-level-dropdown { - min-width: 100px; .dropdown-button { @@ -316,7 +315,8 @@ display: flex; gap: 12px; - .datas__separator {} + // .datas__separator { + // } .disable { cursor: not-allowed; @@ -325,7 +325,6 @@ opacity: 0.5; /* Optional: Makes the button look visually disabled */ } - } } @@ -478,202 +477,280 @@ } } } -} + .machine-mechanics-container { + .machine-mechanics-header { + padding: 8px 12px; + border-top: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); + color: var(--accent-color); + } + .process-list-container { + display: flex; + align-items: center; + gap: 4px; + padding: 8px; + border-bottom: 1px solid var(--border-color); + .label { + margin-right: 8px; + } + .add-new-process { + @include flex-center; + height: 24px; + min-width: 24px; + cursor: pointer; + background: var(--background-color-secondary); + border-radius: #{$border-radius-medium}; + path { + stroke: var(--accent-color); + strokewidth: 1.5px; + } + &:hover { + background: var(--accent-color); + path { + stroke: var(--highlight-accent-color); + } + } + } + } + .machine-mechanics-content-container { + max-height: calc(60vh - (47px - 35px)); + overflow: auto; + overflow-y: scroll; + .header { + @include flex-space-between; + padding: 6px 12px; + padding-right: 6px; -.machine-mechanics-container { - .header { - @include flex-space-between; - padding: 6px 12px; + .add-button { + @include flex-center; + padding: 2px 4px; + background: var(--accent-color); + color: var(--primary-color); + border-radius: #{$border-radius-small}; + cursor: pointer; - .add-button { - @include flex-center; - padding: 2px 4px; - background: var(--accent-color); - color: var(--primary-color); - border-radius: #{$border-radius-small}; - cursor: pointer; + path { + stroke: var(--primary-color); + } + } + } - path { - stroke: var(--primary-color); + .lists-main-container { + margin: 2px 8px; + width: calc(100% - 12px); + margin-right: 4px; + background: var(--background-color-gray); + border-radius: #{$border-radius-small}; + min-height: 120px; + + .list-container { + padding: 4px; + height: calc(100% - 16px); + + .list-item { + @include flex-space-between; + padding: 2px 12px; + width: 100%; + margin: 2px 0; + border-radius: #{$border-radius-small}; + } + + .active { + background: var(--highlight-accent-color); + + .value .input-value { + color: var(--accent-color); + } + + path { + stroke: var(--accent-color); + } + } + + .remove-button { + @include flex-center; + height: 18px; + width: 18px; + cursor: pointer; + border-radius: #{$border-radius-small}; + transform: translateX(4px); + &:hover { + background-color: var(--accent-color); + path { + stroke: var(--primary-color); + } + } + } + } + + .resize-icon { + @include flex-center; + padding: 4px; + cursor: grab; + + &:active { + cursor: grabbing; + } + } + } + + .selected-properties-container { + padding: 12px; + .properties-header { + color: var(--accent-color); + font-weight: var(--font-weight-regular); + padding: 8px 0; + } + .value-field-container { + margin-bottom: 6px; + padding: 0; + @include flex-space-between; + .label { + width: 40%; + } + .regularDropdown-container { + width: 60%; + } + .default { + width: 60%; + } + } + } + + .footer { + @include flex-center; + justify-content: flex-start; + gap: 4px; + padding: 12px; + font-size: var(--font-size-tiny); } } } - - .lists-main-container { - margin: 2px 8px; - width: calc(100% - 16px); - background: var(--background-color-secondary); - border-radius: #{$border-radius-small}; - - .list-container { - min-height: 120px; - padding: 4px; - - .list-item { - @include flex-space-between; - padding: 4px 12px; - width: 100%; - margin: 2px 0; - border-radius: #{$border-radius-small}; - } - - .active { - background: var(--accent-color); - - .value { - color: var(--primary-color); - } - - path { - stroke: var(--primary-color); - } - } - + .global-properties-container, + .analysis-main-container, + .asset-properties-container { + .header { + padding: 8px 12px; + border-top: 1px solid var(--highlight-accent-color); + border-bottom: 1px solid var(--highlight-accent-color); + color: var(--accent-color); + } + .input-container { + @include flex-center; .remove-button { @include flex-center; - height: 12px; - width: 12px; - cursor: pointer; + height: 18px; + width: 18px; + margin-bottom: 6px; + border-radius: 8px 0 0 8px; + &:hover { + background-color: var(--accent-color); + path { + stroke: var(--primary-color); + } + } } } - - .resize-icon { + .optimize-button, + .generate-report-button { @include flex-center; - padding: 4px; - cursor: grab; - - &:active { - cursor: grabbing; + background-color: var(--accent-color); + color: var(--primary-color); + border-radius: #{$border-radius-large}; + padding: 2px; + gap: 4px; + margin: 4px 12px; + cursor: pointer; + font-size: var(--font-size-small); + margin-bottom: 8px; + } + .split { + height: 1px; + background: var(--highlight-accent-color); + margin: 8px; + } + .custom-input-container { + .header { + border: none; + } + .inputs-container { + @include flex-space-between; + padding-bottom: 8px; + .input-container { + padding: 0 12px; + margin-top: 6px; + gap: 6px; + } + } + .custom-input-label { + white-space: nowrap; + } + } + .analysis-content-container { + min-height: 50vh; + max-height: 60vh; + overflow-y: auto; + .dropdown-header-container, + .dropdown-content-container { + padding: 6px 12px; + border-top: 1px solid var(--highlight-accent-color); + } + .input-range-container { + .input-container { + width: 75%; + } + } + } + .buttons-container { + @include flex-space-between; + padding: 12px; + gap: 12px; + input { + border: none; + cursor: pointer; + transition: all 0.2s; + &:hover { + transform: translateY(-2px); + box-shadow: #{$box-shadow-medium}; + outline: 1px solid var(--accent-color); + } + } + .cancel { + background: transparent; + color: var(--accent-color); + } + .submit { + background: var(--accent-color); + color: var(--highlight-accent-color); + } + } + .create-custom-analysis-container { + margin: 6px; + background: var(--background-color-gray); + padding: 12px; + border-radius: #{$border-radius-medium}; + .custom-analysis-header { + font-weight: var(--font-weight-medium); + } + .content { + padding: 12px 0; + font-size: var(--font-size-small); + span { + font-size: inherit; + color: var(--accent-color); + } + } + .input { + display: flex; + flex-direction: row-reverse; + input { + width: fit-content; + background: var(--accent-color); + color: var(--highlight-accent-color); + padding: 2px 8px; + cursor: pointer; + } } } - } - - .selected-properties-container { - padding: 12px; - } - - .footer { - @include flex-center; - justify-content: flex-start; - gap: 4px; - padding: 12px; - font-size: var(--font-size-tiny); } } - - - - - - - - - - - - - -.multi-level-dropdown { - position: relative; - display: inline-block; - - .dropdown-button { - width: 100%; - background-color: var(--background-color) !important; - border: 1px solid var(--border-color) !important; - padding: 5px 10px; - - - // font-size: 12px; - cursor: pointer; - border-radius: 5px; - transition: background-color 0.3s ease; - - &:hover { - background-color: #333333; - } - - &.open { - background-color: #333333; - } - } - - .dropdown-menu { - position: absolute; - top: 100%; - left: 0; - background-color: #ffffff; - border: 1px solid #cccccc; - border-radius: 5px; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - z-index: 1000; - min-width: 200px; - overflow: auto; - max-height: 600px; - - .dropdown-content { - display: flex; - flex-direction: column; - gap: 6px; - - .nested-dropdown { - // &:first-child{ - margin-left: 0; - // } - } - - padding: 10px; - } - - .dropdown-item { - display: block; - padding: 5px 10px; - text-decoration: none; - color: #000000; - font-size: 14px; - cursor: pointer; - transition: background-color 0.3s ease; - - &:hover { - background-color: #f0f0f0; - } - } - - .nested-dropdown { - margin-left: 20px; - - .dropdown-trigger { - display: flex; - align-items: center; - justify-content: space-between; - padding: 5px 10px; - cursor: pointer; - font-size: 14px; - color: #000000; - transition: background-color 0.3s ease; - - &:hover { - background-color: #f0f0f0; - } - - &.open { - background-color: #e0e0e0; - } - - .icon { - font-size: 12px; - margin-left: 5px; - } - } - - .submenu { - margin-top: 5px; - padding-left: 20px; - border-left: 2px solid #cccccc; - display: flex; - flex-direction: column; - gap: 6px; - } - } - } -} \ No newline at end of file diff --git a/app/src/styles/layout/toast.scss b/app/src/styles/layout/toast.scss new file mode 100644 index 0000000..a3f21de --- /dev/null +++ b/app/src/styles/layout/toast.scss @@ -0,0 +1,83 @@ +.toast-container { + position: fixed; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 10px; +} + +.toast-container.bottom-center { + bottom: 20px; + left: 50%; + transform: translateX(-50%); +} + +.toast-container.bottom-left { + bottom: 20px; + left: 20px; +} + +.toast-container.bottom-right { + bottom: 20px; + right: 20px; +} + +.toast-container.top-center { + top: 20px; + left: 50%; + transform: translateX(-50%); +} + +.toast-container.top-left { + top: 20px; + left: 20px; +} + +.toast-container.top-right { + top: 20px; + right: 20px; +} + +.toast { + padding: 10px 20px; + border-radius: 5px; + color: var(--primary-color); + cursor: pointer; + animation: fadeIn 0.3s, fadeOut 0.5s 2.5s; +} + +.toast.success { + background-color: #4caf50; +} + +.toast.error { + background-color: #f44336; +} + +.toast.info { + background-color: #2196f3; +} + +.toast.warning { + background-color: #ff9800; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} diff --git a/app/src/styles/main.scss b/app/src/styles/main.scss index 90f6419..f3c562d 100644 --- a/app/src/styles/main.scss +++ b/app/src/styles/main.scss @@ -18,12 +18,15 @@ @use 'components/moduleToggle'; @use 'components/templates'; @use 'components/tools'; -@use 'components/regularDropDown'; @use 'components/visualization/floating/energyConsumed'; // layout +@use 'layout/loading'; @use 'layout/sidebar'; +@use 'layout/popup'; +@use 'layout/toast'; // pages @use 'pages/home'; -@use 'pages/realTimeViz'; \ No newline at end of file +@use 'pages/realTimeViz'; +@use 'pages/userAuth'; \ No newline at end of file diff --git a/app/src/styles/pages/userAuth.scss b/app/src/styles/pages/userAuth.scss new file mode 100644 index 0000000..fd4048c --- /dev/null +++ b/app/src/styles/pages/userAuth.scss @@ -0,0 +1,190 @@ +@use "../abstracts/variables" as *; +@use "../abstracts/mixins" as *; + +.auth-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px; + color: var(--text-color); + height: 100vh; + background-color: var(--background-color); + background-color: #fcfdfd; + position: relative; + z-index: 1; + .logo-icon { + width: 80px; + height: 80px; + margin-bottom: 20px; + @include flex-center; + } + + h1 { + color: var(--text-color); + font-size: 24px; + margin-bottom: 10px; + font-weight: var(--font-weight-medium); + } + + p { + font-size: 14px; + color: var(--text-color); + margin-bottom: 20px; + + .link { + color: var(--accent-color); + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + } + + .google-login { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 350px; + padding: 10px; + margin-bottom: 20px; + border: 1px solid var(--accent-color); + border-radius: #{$border-radius-extra-large}; + background: transparent; + color: var(--accent-color); + font-size: 14px; + outline: none; + cursor: pointer; + + .google-icon { + color: var(--accent-color); + font-weight: bold; + font-size: 16px; + margin-right: 10px; + } + + &:hover { + background: var(--background-color); + } + } + + .error-message { + color: #ff4d4f; + font-size: 12px; + margin-bottom: 10px; + } + + .auth-form { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + max-width: 350px; + + input { + width: 100%; + padding: 10px 18px; + margin-bottom: 10px; + border: 1px solid var(--border-color); + border-radius: #{$border-radius-extra-large}; + background: var(--background-color); + font-size: 14px; + color: #333; + + &:focus { + border-color: var(--accent-color); + outline: none; + } + } + + .password-container { + position: relative; + width: 100%; + + .toggle-password { + @include flex-center; + position: absolute; + top: 20px; + right: 10px; + height: 22px; + width: 22px; + transform: translateY(-50%); + background: none; + border: none; + outline: none; + cursor: pointer; + font-size: 16px; + } + } + + .continue-button { + width: 100%; + padding: 10px; + background: var(--accent-gradient-color); + color: var(--background-color); + font-size: 14px; + border: none; + outline: none; + border-radius: #{$border-radius-extra-large}; + cursor: pointer; + + &:hover { + opacity: 0.9; + } + } + } + .policy-checkbox { + @include flex-center; + gap: 6px; + margin: 18px 0; + margin-top: 8px; + justify-content: flex-start; + width: 100%; + padding: 0 6px; + input { + width: 15px; + height: 15px; + margin: 0; + accent-color: var(--accent-color); + } + } + + .policy { + font-size: 12px; + margin-top: 20px; + text-align: center; + line-height: 1.5; + width: 320px; + text-align: center; + + .link { + color: var(--accent-color); + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + &::after { + content: ""; + position: absolute; + background: radial-gradient( + circle, + #bfe0f8 0%, + #e9ebff 46%, + #e2acff 100% + ); + height: 50vh; + width: 50vw; + top: 0; + left: 0; + top: 50%; + left: 50%; + opacity: 0.5; + transform: translate(-50%, -50%); + border-radius: 50%; + filter: blur(200px); + z-index: -1; + } + } +} diff --git a/app/src/types/analysis.d.ts b/app/src/types/analysis.d.ts new file mode 100644 index 0000000..02ce1c1 --- /dev/null +++ b/app/src/types/analysis.d.ts @@ -0,0 +1,11 @@ +type Preset = { + type: string; + inputs: { + label: string; + activeOption: string; + }; +}; + +export type AnalysisPresetsType = { + [key: string]: Preset[]; +}; diff --git a/app/src/types/declarations.d.ts b/app/src/types/declarations.d.ts new file mode 100644 index 0000000..00594f0 --- /dev/null +++ b/app/src/types/declarations.d.ts @@ -0,0 +1,8 @@ +declare module '*.png'; +declare module '*.svg'; +declare module '*.hdr'; +declare module '*.css'; +declare module '*.scss'; + +declare module '*.glb'; +declare module '*.gltf'; \ No newline at end of file diff --git a/app/src/types/users.d.ts b/app/src/types/users.d.ts new file mode 100644 index 0000000..9e8c35a --- /dev/null +++ b/app/src/types/users.d.ts @@ -0,0 +1,11 @@ +export interface User { + name: string; + email: string; + profileImage: string; + color: string; + access: string; +} + +type AccessOption = { + option: string; +}; \ No newline at end of file diff --git a/app/src/utils/contextmenuHandler.ts b/app/src/utils/contextmenuHandler.ts new file mode 100644 index 0000000..1f71466 --- /dev/null +++ b/app/src/utils/contextmenuHandler.ts @@ -0,0 +1,137 @@ +import React, { useState } from "react"; + +interface HandleDivProps { + switchesRef: React.RefObject; + setMenuLeftPosition: React.Dispatch>; + setMenuTopPosition: React.Dispatch>; + setMenuVisible: React.Dispatch>; +} +// The functional component that handles right-click (contextmenu) events +export default function ContextMenuHandler({ + switchesRef, + setMenuLeftPosition, + setMenuTopPosition, + setMenuVisible, +}: HandleDivProps) { + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + + // Function to handle the contextmenu event when a right-click happens + const handleClick = (event: MouseEvent) => { + event.preventDefault(); + const targets = event.target as HTMLElement; + const isInsideSwitches = switchesRef.current?.contains( + targets as Node + ); + const rect = switchesRef.current?.getBoundingClientRect(); + if (!rect) return; + + const totalHeight = rect.height + rect.top; + + const totalWidth = rect.width + rect.left; + + // Calculate the new position for the context menu + if (isInsideSwitches) { + const yPosition = event.clientY; + const xPosition = event.clientX; + //for top contextmenu handling + + if ( + totalHeight - yPosition > 20 && + totalHeight - yPosition < 260 + ) { + const minTop = yPosition - 110; + setMenuTopPosition(minTop); + } else if ( + totalHeight - yPosition >= 260 && + yPosition > height - 73 + ) { + const minTop = yPosition + 115; + setMenuTopPosition(minTop); + } + + // for top contextmenu handling + if ( + totalWidth - xPosition > 500 && + totalWidth - xPosition < 900 + ) { + const minLeft = xPosition + 80; + setMenuLeftPosition(minLeft); + } else if ( + totalWidth - xPosition > 10 && + totalWidth - xPosition > 150 + ) { + const minLeft = xPosition + 80; + setMenuLeftPosition(minLeft); + } else { + const minLeft = xPosition - 80; + setMenuLeftPosition(minLeft); + } + // setMenuVisible(true); + } else { + setMenuVisible(false); + } + }; + React.useEffect(() => { + const element = switchesRef.current; + + // Create a resize observer + const resizeObserver = new ResizeObserver((entries) => { + if (entries.length > 0) { + // Update the width state with the new width of the element + const { width, height } = entries[0].contentRect; + setWidth(width); + setHeight(height); + } + }); + + // Start observing the element's width changes + if (element) { + resizeObserver.observe(element); + } + + // Cleanup observer on component unmount + return () => { + if (element) { + resizeObserver.unobserve(element); + + } + }; + }, [height, width]); + + React.useEffect(() => { + let drag = false; + let isRightMouseDown = false; + + const handleDown = (event: MouseEvent) => { + if (event.button === 2) { + isRightMouseDown = true; + drag = false; + } + } + + const handleUp = (event: MouseEvent) => { + if (event.button === 2) { + isRightMouseDown = false; + if (!drag) { + handleClick(event); + } + }; + } + + const handleMove = (event: MouseEvent) => { + if (isRightMouseDown) { drag = true; }; + } + + document.addEventListener("mousedown", handleDown); + document.addEventListener("mousemove", handleMove); + document.addEventListener("mouseup", handleUp); + + return () => { + document.removeEventListener("mousedown", handleDown); + document.removeEventListener("mousemove", handleMove); + document.removeEventListener("mouseup", handleUp); + }; + }, []); + return null; +} diff --git a/app/src/utils/outerClick.ts b/app/src/utils/outerClick.ts new file mode 100644 index 0000000..c5a6f74 --- /dev/null +++ b/app/src/utils/outerClick.ts @@ -0,0 +1,29 @@ +import React from "react"; + +interface OuterClickProps { + contextClassName: string; + setMenuVisible: React.Dispatch>; +} + +export default function OuterClick({ + contextClassName, + setMenuVisible, +}: OuterClickProps) { + const handleClick = (event: MouseEvent) => { + const targets = event.target as HTMLElement; + // Check if the click is outside the selectable-dropdown-wrapper + if (!targets.closest(`.${contextClassName}`)) { + setMenuVisible(false); // Close the menu by updating the state + } + }; + + // Add event listener on mount and remove it on unmount + React.useEffect(() => { + document.addEventListener("click", handleClick); + return () => { + document.removeEventListener("click", handleClick); + }; + }, []); + + return null; // This component doesn't render anything +} diff --git a/app/vite.config.ts b/app/vite.config.ts index aeec58e..27940aa 100644 --- a/app/vite.config.ts +++ b/app/vite.config.ts @@ -7,5 +7,8 @@ export default defineConfig({ server: { port: 8200, host: true, + }, + optimizeDeps: { + exclude: ["@react-oauth/google"], // Exclude the problematic library }, });