From 29754af51130fd365aa5e5719d0efb8edf113e7c Mon Sep 17 00:00:00 2001 From: Dennis Chen <41879777+chennisden@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:15:36 -0700 Subject: [PATCH] Add URDF viewer (#155) * urdf * drei_bootstrap_error * converted urdf component to typsecript * Fix * custom button and urdf ui * completed urdf component * format * conditional rendering of carousel arrows and fix arrows * CSS improvements * user interaction * url_params * urdf form * added urdf packages interface * fix @ts-nocheck associated lints * small changes * minor fixes --------- Co-authored-by: Isaac Light --- frontend/package-lock.json | 469 +++++++++++++++++- frontend/package.json | 2 + frontend/src/components/PartForm.tsx | 13 +- frontend/src/components/RobotForm.tsx | 111 ++++- .../components/auth/EmailAuthComponent.tsx | 16 +- .../components/files/InputerURDFLoader.tsx | 117 +++++ frontend/src/components/files/TCButton.tsx | 16 + frontend/src/components/files/URDFLoader.tsx | 111 +++++ frontend/src/components/files/UploadImage.tsx | 7 +- frontend/src/components/nav/Sidebar.tsx | 7 +- frontend/src/hooks/api.tsx | 7 + frontend/src/pages/EditRobotForm.tsx | 12 +- frontend/src/pages/Forgot.tsx | 7 +- frontend/src/pages/Home.tsx | 19 +- frontend/src/pages/Login.tsx | 5 +- frontend/src/pages/NewRobot.tsx | 10 +- frontend/src/pages/PartDetails.tsx | 199 ++++---- frontend/src/pages/Parts.tsx | 2 +- frontend/src/pages/Register.tsx | 5 +- frontend/src/pages/RegistrationEmail.tsx | 5 +- frontend/src/pages/ResetPassword.tsx | 7 +- frontend/src/pages/RobotDetails.tsx | 325 +++++++----- frontend/src/pages/Robots.tsx | 2 +- frontend/src/pages/TestImages.tsx | 6 +- frontend/src/pages/YourParts.tsx | 2 +- frontend/src/pages/YourRobots.tsx | 2 +- store/app/crud/robots.py | 42 +- store/app/model.py | 7 + store/app/routers/robot.py | 8 +- 29 files changed, 1222 insertions(+), 319 deletions(-) create mode 100644 frontend/src/components/files/InputerURDFLoader.tsx create mode 100644 frontend/src/components/files/TCButton.tsx create mode 100644 frontend/src/components/files/URDFLoader.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8460aa01..12cf1a70 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@react-three/drei": "^9.107.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -25,6 +26,7 @@ "react-scripts": "5.0.1", "react-spring": "^9.7.3", "typescript": "^4.9.5", + "urdf-loader": "^0.12.1", "uuid": "^10.0.0", "web-vitals": "^2.1.4" }, @@ -4009,6 +4011,22 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", "license": "MIT" }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz", + "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.0.5.tgz", + "integrity": "sha512-53sCTG4FaJBaAq/tcufARtVYDMDGqyBT9i7F453pWGhZ5LqubDHDWtYoHo9VhQqMcHTEexdJqSsR58y+9HVmQA==", + "dependencies": { + "promise-worker-transferable": "^1.0.4" + }, + "peerDependencies": { + "three": ">= 0.159.0" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -5676,6 +5694,11 @@ "react-konva": "^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0" } }, + "node_modules/@react-spring/rafz": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.6.1.tgz", + "integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==" + }, "node_modules/@react-spring/shared": { "version": "9.7.3", "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", @@ -5740,6 +5763,122 @@ "zdog": ">=1.0" } }, + "node_modules/@react-three/drei": { + "version": "9.107.0", + "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-9.107.0.tgz", + "integrity": "sha512-8AxMfk3NmE/tPwN/wAcTyYIZgi4YCsm+ID3vEVb28CCJ4bo4b2UZPWfrMskO7zM1T2ePZaQwmN4Q+cxSpYWn0A==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@mediapipe/tasks-vision": "0.10.8", + "@monogrid/gainmap-js": "^3.0.5", + "@react-spring/three": "~9.6.1", + "@use-gesture/react": "^10.2.24", + "camera-controls": "^2.4.2", + "cross-env": "^7.0.3", + "detect-gpu": "^5.0.28", + "glsl-noise": "^0.0.0", + "hls.js": "1.3.5", + "maath": "^0.10.7", + "meshline": "^3.1.6", + "react-composer": "^5.0.3", + "stats-gl": "^2.0.0", + "stats.js": "^0.17.0", + "suspend-react": "^0.1.3", + "three-mesh-bvh": "^0.7.0", + "three-stdlib": "^2.29.9", + "troika-three-text": "^0.49.0", + "tunnel-rat": "^0.1.2", + "utility-types": "^3.10.0", + "uuid": "^9.0.1", + "zustand": "^3.7.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=8.0", + "react": ">=18.0", + "react-dom": ">=18.0", + "three": ">=0.137" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/@react-three/drei/node_modules/@react-spring/animated": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.6.1.tgz", + "integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==", + "dependencies": { + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-three/drei/node_modules/@react-spring/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.6.1.tgz", + "integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/rafz": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-three/drei/node_modules/@react-spring/shared": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.6.1.tgz", + "integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==", + "dependencies": { + "@react-spring/rafz": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-three/drei/node_modules/@react-spring/three": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.6.1.tgz", + "integrity": "sha512-Tyw2YhZPKJAX3t2FcqvpLRb71CyTe1GvT3V+i+xJzfALgpk10uPGdGaQQ5Xrzmok1340DAeg2pR/MCfaW7b8AA==", + "dependencies": { + "@react-spring/animated": "~9.6.1", + "@react-spring/core": "~9.6.1", + "@react-spring/shared": "~9.6.1", + "@react-spring/types": "~9.6.1" + }, + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" + } + }, + "node_modules/@react-three/drei/node_modules/@react-spring/types": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.6.1.tgz", + "integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==" + }, + "node_modules/@react-three/drei/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@react-three/fiber": { "version": "8.16.8", "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-8.16.8.tgz", @@ -6625,6 +6764,11 @@ "node": ">=10.13.0" } }, + "node_modules/@tweenjs/tween.js": { + "version": "23.1.2", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.2.tgz", + "integrity": "sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -6719,6 +6863,11 @@ "@types/ms": "*" } }, + "node_modules/@types/draco3d": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", + "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==" + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -6931,6 +7080,11 @@ "@types/node": "*" } }, + "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==" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -7071,6 +7225,11 @@ "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==" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -7080,6 +7239,19 @@ "@types/jest": "*" } }, + "node_modules/@types/three": { + "version": "0.165.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.165.0.tgz", + "integrity": "sha512-AJK8JZAFNBF0kBXiAIl5pggYlzAGGA8geVYQXAcPCEDRbyA+oEjkpUBcJJrtNz6IiALwzGexFJGZG2yV3WsYBw==", + "peer": true, + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -7108,8 +7280,7 @@ "node_modules/@types/webxr": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.16.tgz", - "integrity": "sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA==", - "peer": true + "integrity": "sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA==" }, "node_modules/@types/ws": { "version": "8.5.10", @@ -7396,6 +7567,22 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "license": "ISC" }, + "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==" + }, + "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==", + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", @@ -8659,6 +8846,14 @@ "node": ">= 8.0.0" } }, + "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==", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -9047,6 +9242,14 @@ "node": ">= 6" } }, + "node_modules/camera-controls": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.8.5.tgz", + "integrity": "sha512-7VTwRk7Nu1nRKsY7bEt9HVBfKt8DETvzyYhLN4OW26OByBayMDB5fUaNcPI+z++vG23RH5yqn6ZRhZcgLQy2rA==", + "peerDependencies": { + "three": ">=0.126.1" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -9798,6 +10001,23 @@ "node": ">=8" } }, + "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==", + "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.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -10579,6 +10799,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-gpu": { + "version": "5.0.38", + "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.38.tgz", + "integrity": "sha512-36QeGHSXYcJ/RfrnPEScR8GDprbXFG4ZhXsfVNVHztZr38+fRxgHnJl3CjYXXjbeRUhu3ZZBJh6Lg0A9v0Qd8A==", + "dependencies": { + "webgl-constants": "^1.1.1" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -10824,6 +11052,11 @@ "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", "license": "BSD-2-Clause" }, + "node_modules/draco3d": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", + "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==" + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -12325,6 +12558,11 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -13112,6 +13350,11 @@ "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==" + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -13329,6 +13572,11 @@ "node": ">= 8" } }, + "node_modules/hls.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.3.5.tgz", + "integrity": "sha512-uybAvKS6uDe0MnWNEPnO0krWVr+8m2R0hJ/viql8H3MVK+itq8gGQuIYoFHL3rECkIpNH98Lw8YuuWMKZxp3Ew==" + }, "node_modules/holderjs": { "version": "2.9.9", "resolved": "https://registry.npmjs.org/holderjs/-/holderjs-2.9.9.tgz", @@ -13715,6 +13963,11 @@ "node": ">=16.x" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -14231,6 +14484,11 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "license": "MIT" }, + "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==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -18036,6 +18294,14 @@ "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==", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -18456,6 +18722,15 @@ "lz-string": "bin/bin.js" } }, + "node_modules/maath": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.7.tgz", + "integrity": "sha512-zQ2xd7dNOIVTjAS+hj22fyj1EFYmOJX6tzKjZ92r6WDoq8hyFxjuGA2q950tmR4iC/EKXoMQdSipkaJVuUHDTg==", + "peerDependencies": { + "@types/three": ">=0.144.0", + "three": ">=0.144.0" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -18721,6 +18996,19 @@ "node": ">= 8" } }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "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==" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -22229,6 +22517,11 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -22351,6 +22644,15 @@ "asap": "~2.0.6" } }, + "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==", + "dependencies": { + "is-promise": "^2.1.0", + "lie": "^3.0.2" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -22692,6 +22994,17 @@ "react": ">=16.8.6" } }, + "node_modules/react-composer": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/react-composer/-/react-composer-5.0.3.tgz", + "integrity": "sha512-1uWd07EME6XZvMfapwZmc7NgCZqDemcvicRi3wMJzXsQLvZ3L7fTHVyPy1bZdnWXM4iPjYuNE+uJ41MLKeTtnA==", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -26094,6 +26407,31 @@ "escodegen": "^1.8.1" } }, + "node_modules/stats-gl": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.2.8.tgz", + "integrity": "sha512-94G5nZvduDmzxBS7K0lYnynYwreZpkknD8g5dZmU6mpwIhy3caCrjAm11Qm1cbyx7mqix7Fp00RkbsonzKWnoQ==", + "dependencies": { + "@types/three": "^0.163.0" + } + }, + "node_modules/stats-gl/node_modules/@types/three": { + "version": "0.163.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.163.0.tgz", + "integrity": "sha512-uIdDhsXRpQiBUkflBS/i1l3JX14fW6Ot9csed60nfbZNXHDTRsnV2xnTVwXcgbvTiboAR4IW+t+lTL5f1rqIqA==", + "dependencies": { + "@tweenjs/tween.js": "~23.1.1", + "@types/stats.js": "*", + "@types/webxr": "*", + "fflate": "~0.8.2", + "meshoptimizer": "~0.18.1" + } + }, + "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==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -26555,7 +26893,6 @@ "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==", - "peer": true, "peerDependencies": { "react": ">=17.0" } @@ -26990,6 +27327,35 @@ "integrity": "sha512-cc96IlVYGydeceu0e5xq70H8/yoVT/tXBxV/W8A/U6uOq7DXc4/s1Mkmnu6SqoYGhSRWWYFOhVwvq6V0VtbplA==", "peer": true }, + "node_modules/three-mesh-bvh": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.7.5.tgz", + "integrity": "sha512-WDd77RklE52pZSKZx8sDXzrd2JCF/gL/hugFvsIBylpMRlJxxwesKn2rW7TcQZ809NocDVkQx1UJo9pJtVAPYg==", + "peerDependencies": { + "three": ">= 0.151.0" + } + }, + "node_modules/three-stdlib": { + "version": "2.30.3", + "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.30.3.tgz", + "integrity": "sha512-rYr8PqMljMza+Ct8kQk90Y7y+YcWoPu1thfYv5YGCp0hytNRbxSQWXY4GpdTGymCj3bDggEBpxso53C3pPwhIw==", + "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==" + }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -27140,6 +27506,33 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/troika-three-text": { + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.49.1.tgz", + "integrity": "sha512-lXGWxgjJP9kw4i4Wh+0k0Q/7cRfS6iOME4knKht/KozPu9GcFA9NnNpRvehIhrUawq9B0ZRw+0oiFHgRO+4Wig==", + "dependencies": { + "bidi-js": "^1.0.2", + "troika-three-utils": "^0.49.0", + "troika-worker-utils": "^0.49.0", + "webgl-sdf-generator": "1.1.1" + }, + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-three-utils": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.49.0.tgz", + "integrity": "sha512-umitFL4cT+Fm/uONmaQEq4oZlyRHWwVClaS6ZrdcueRvwc2w+cpNQ47LlJKJswpqtMFWbEhOLy0TekmcPZOdYA==", + "peerDependencies": { + "three": ">=0.125.0" + } + }, + "node_modules/troika-worker-utils": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.49.0.tgz", + "integrity": "sha512-1xZHoJrG0HFfCvT/iyN41DvI/nRykiBtHqFkGaGgJwq5iXfIZFBiPPEHFpPpgyKM3Oo5ITHXP5wM2TNQszYdVg==" + }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -27296,6 +27689,41 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "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==", + "dependencies": { + "zustand": "^4.3.2" + } + }, + "node_modules/tunnel-rat/node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "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/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -27976,6 +28404,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/urdf-loader": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/urdf-loader/-/urdf-loader-0.12.1.tgz", + "integrity": "sha512-Sae8dmekFD4ERZYDtpei8mxmuMxqy+YnjN2PfI1TsDz+9QIXL4PyPrvYbXcJj2h9MfL4aS6oUc2j3ap5jRFWfA==", + "peerDependencies": { + "three": ">=0.152.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -27995,6 +28431,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -28022,6 +28466,14 @@ "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", "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==", + "engines": { + "node": ">= 4" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -28186,6 +28638,16 @@ "integrity": "sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==", "license": "Apache-2.0" }, + "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==" + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -29197,7 +29659,6 @@ "version": "3.7.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.7.2.tgz", "integrity": "sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==", - "peer": true, "engines": { "node": ">=12.7.0" }, diff --git a/frontend/package.json b/frontend/package.json index 68cd70a1..23d55b2a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@react-three/drei": "^9.107.0", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -20,6 +21,7 @@ "react-scripts": "5.0.1", "react-spring": "^9.7.3", "typescript": "^4.9.5", + "urdf-loader": "^0.12.1", "uuid": "^10.0.0", "web-vitals": "^2.1.4" }, diff --git a/frontend/src/components/PartForm.tsx b/frontend/src/components/PartForm.tsx index da1bfa0e..f8f24b66 100644 --- a/frontend/src/components/PartForm.tsx +++ b/frontend/src/components/PartForm.tsx @@ -1,7 +1,8 @@ +import TCButton from "components/files/TCButton"; import { Image } from "hooks/api"; import { Theme } from "hooks/theme"; import { ChangeEvent, Dispatch, FormEvent, SetStateAction } from "react"; -import { Button, Col, Form, Row } from "react-bootstrap"; +import { Col, Form, Row } from "react-bootstrap"; import ImageUploadComponent from "./files/UploadImage"; interface PartFormProps { @@ -100,27 +101,27 @@ const PartForm: React.FC = ({ /> - + ))} - + - + Submit diff --git a/frontend/src/components/RobotForm.tsx b/frontend/src/components/RobotForm.tsx index ecb9aae8..88790e29 100644 --- a/frontend/src/components/RobotForm.tsx +++ b/frontend/src/components/RobotForm.tsx @@ -1,7 +1,8 @@ -import { Bom, Image, Part } from "hooks/api"; +import TCButton from "components/files/TCButton"; +import { Bom, Image, Package, Part } from "hooks/api"; import { Theme } from "hooks/theme"; import { ChangeEvent, Dispatch, FormEvent, SetStateAction } from "react"; -import { Button, Col, Form, Row } from "react-bootstrap"; +import { Col, Form, Row } from "react-bootstrap"; import ImageUploadComponent from "./files/UploadImage"; interface RobotFormProps { @@ -24,6 +25,10 @@ interface RobotFormProps { handleSubmit: (event: FormEvent) => void; robot_images: Image[]; setImages: Dispatch>; + robotURDF: string; + setURDF: Dispatch>; + robot_packages: Package[]; + setPackages: Dispatch>; } const RobotForm: React.FC = ({ @@ -46,7 +51,27 @@ const RobotForm: React.FC = ({ handleSubmit, robot_images, setImages, + robotURDF, + setURDF, + robot_packages, + setPackages, }) => { + const handlePackageChange = ( + index: number, + e: ChangeEvent, + ) => { + const { name, value } = e.target; + const newPackages = [...robot_packages]; + newPackages[index][name as keyof Package] = value; + setPackages(newPackages); + }; + const handleAddPackage = () => { + setPackages([...robot_packages, { name: "", url: "" }]); + }; + const handleRemovePackage = (index: number) => { + const newPackages = robot_packages.filter((_, i) => i !== index); + setPackages(newPackages); + }; const handleImageChange = ( index: number, e: ChangeEvent, @@ -112,7 +137,7 @@ const RobotForm: React.FC = ({ value={robot_name} required /> - + = ({ }} value={robot_height} /> - + = ({ }} value={robot_weight} /> - + = ({ value={robot_description} required /> +

URDF (Optional)

+ + { + setURDF(e.target.value); + }} + value={robotURDF} + /> + + {robot_packages && + robot_packages.map((pkg, index) => ( + + + + handlePackageChange(index, e)} + required + /> + handlePackageChange(index, e)} + required + /> + + + handleRemovePackage(index)} + > + Remove + + + + ))} + + + Add Package + + +

Images

{robot_images.map((image, index) => ( @@ -175,24 +258,24 @@ const RobotForm: React.FC = ({ /> - + ))} - +

Bill of Materials

{robot_bom.map((bom, index) => ( @@ -229,27 +312,27 @@ const RobotForm: React.FC = ({ /> - + ))} - + - + Submit diff --git a/frontend/src/components/auth/EmailAuthComponent.tsx b/frontend/src/components/auth/EmailAuthComponent.tsx index 19243f37..58a5a6ed 100644 --- a/frontend/src/components/auth/EmailAuthComponent.tsx +++ b/frontend/src/components/auth/EmailAuthComponent.tsx @@ -1,20 +1,12 @@ import { faEnvelope } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import TCButton from "components/files/TCButton"; import { humanReadableError } from "constants/backend"; import { useAlertQueue } from "hooks/alerts"; import { useAuthentication } from "hooks/auth"; import { useRef, useState } from "react"; -import { - Button, - Col, - Form, - InputGroup, - Overlay, - Row, - Tooltip, -} from "react-bootstrap"; +import { Col, Form, InputGroup, Overlay, Row, Tooltip } from "react-bootstrap"; import { CheckCircle } from "react-bootstrap-icons"; - const isValidEmail = (email: string) => { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); }; @@ -79,7 +71,7 @@ const EmailAuthComponent = () => { value={email} disabled={isDisabled} /> - + 3 && !isValidEmail(email)} diff --git a/frontend/src/components/files/InputerURDFLoader.tsx b/frontend/src/components/files/InputerURDFLoader.tsx new file mode 100644 index 00000000..ea3610c7 --- /dev/null +++ b/frontend/src/components/files/InputerURDFLoader.tsx @@ -0,0 +1,117 @@ +/* eslint-disable */ +// @ts-nocheck +import { OrbitControls } from "@react-three/drei"; +import { Canvas, useLoader } from "@react-three/fiber"; +import React, { Suspense, useRef } from "react"; +import * as THREE from "three"; +import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js"; +import URDFLoader from "urdf-loader"; + +interface LoadModelProps { + filepath: string; + packages: string[] | null; +} + +const LoadModel: React.FC = ({ filepath, packages }) => { + const ref = useRef(null); + //@ts-ignore + const robot = useLoader(URDFLoader, filepath, (loader) => { + // Configure loader + loader.loadMeshFunc = ( + path: string, + manager: THREE.LoadingManager, + done: (mesh: THREE.Object3D | null, err?: Error | undefined) => void, + ) => { + new STLLoader(manager).load( + path, + (result) => { + const material = new THREE.MeshPhongMaterial(); + const mesh = new THREE.Mesh(result, material); + done(mesh); + }, + undefined, + (err) => done(null, err as Error | undefined), + ); + }; + loader.fetchOptions = { + headers: { Accept: "application/vnd.github.v3.raw" }, + }; + + loader.packages = loader.packages || {}; + + // loader.packages = { + // "r2_description": + // "https://raw.githubusercontent.com/gkjohnson/nasa-urdf-robots/master/r2_description", + // }; + if (packages) { + for (let i = 0; i < packages.length; i += 2) { + loader.packages[packages[i]] = packages[i + 1]; + } + } + }); + + // Check for load errors + if (!robot) { + throw new Error("Failed to load model"); + } + + return ( + + + + ); +}; +interface URDFComponentProps { + url: string | null; + packages: string[] | null; +} +export const InputerURDFComponent: React.FC = ({ + url, + packages, +}) => { + const modelPath = url ? url : ""; + // "https://raw.githubusercontent.com/vrtnis/robot-web-viewer/main/public/urdf/robot.urdf"; + // "https://raw.githubusercontent.com/gkjohnson/nasa-urdf-robots/master/r2_description/robots/r2c1.urdf"; + // "https://raw.githubusercontent.com/adubredu/DigitRobot.jl/main/urdf/digit_model.urdf"; + // "https://raw.githubusercontent.com/openai/roboschool/1.0.49/roboschool/models_robot/atlas_description/urdf/atlas_v4_with_multisense.urdf"; + // "https://raw.githubusercontent.com/vrtnis/robot-web-viewer/main/public/urdf/robot.urdf"; + + const containerStyle = { + width: "50vw", + height: "50vh", + backgroundColor: "#272727", + }; + return ( +
+ + + + + + + + + + +
+ ); +}; diff --git a/frontend/src/components/files/TCButton.tsx b/frontend/src/components/files/TCButton.tsx new file mode 100644 index 00000000..5da6998c --- /dev/null +++ b/frontend/src/components/files/TCButton.tsx @@ -0,0 +1,16 @@ +/* eslint-disable */ +// @ts-nocheck +import { forwardRef } from "react"; +import { Button, ButtonProps } from "react-bootstrap"; + +const TCButton = forwardRef((props, ref) => { + return ( + + ); +}); + +TCButton.displayName = "TCButton"; + +export default TCButton; diff --git a/frontend/src/components/files/URDFLoader.tsx b/frontend/src/components/files/URDFLoader.tsx new file mode 100644 index 00000000..bf93f4ff --- /dev/null +++ b/frontend/src/components/files/URDFLoader.tsx @@ -0,0 +1,111 @@ +import { OrbitControls } from "@react-three/drei"; +import { Canvas, useLoader } from "@react-three/fiber"; +import React, { Suspense, useRef } from "react"; +import * as THREE from "three"; +import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js"; +import URDFLoader from "urdf-loader"; + +interface LoadModelProps { + filepath: string; +} + +const LoadModel: React.FC = ({ filepath }) => { + const ref = useRef(null); + + /* eslint-disable */ + // @ts-ignore + const robot = useLoader(URDFLoader, filepath, (loader) => { + // Configure loader + loader.loadMeshFunc = ( + path: string, + manager: THREE.LoadingManager, + done: (mesh: THREE.Object3D | null, err?: Error | undefined) => void, + ) => { + new STLLoader(manager).load( + path, + (result) => { + const material = new THREE.MeshPhongMaterial(); + const mesh = new THREE.Mesh(result, material); + done(mesh); + }, + undefined, + (err) => done(null, err as Error | undefined), + ); + }; + loader.fetchOptions = { + headers: { Accept: "application/vnd.github.v3.raw" }, + packages: {} as { [key: string]: string }, + }; + if (typeof loader.packages !== "object") { + loader.packages = {}; + } + const urls = [ + "https://raw.githubusercontent.com/openai/roboschool/1.0.49/roboschool/models_robot/atlas_description/urdf/atlas_v4_with_multisense.urdf", + "atlas_description", + "https://raw.githubusercontent.com/openai/roboschool/1.0.49/roboschool/models_robot/atlas_description", + ]; + for (let i = 0; i < urls.length; i += 2) { + loader.packages[urls[i]] = urls[i + 1]; + } + // loader.packages = { + // atlas_description: + // "https://raw.githubusercontent.com/openai/roboschool/1.0.49/roboschool/models_robot/atlas_description", + // r2_description: + // "https://raw.githubusercontent.com/gkjohnson/nasa-urdf-robots/master/r2_description", + // urdf: "https://raw.githubusercontent.com/adubredu/DigitRobot.jl/main/urdf", + // multisense_sl_description: + // "https://raw.githubusercontent.com/openai/roboschool/1.0.49/roboschool/models_robot/multisense_sl_description", + // }; + }) as THREE.Group; + + return ( + + + + ); +}; + +export const URDFComponent = () => { + // Parse URL parameter + const urlParams = new URLSearchParams(window.location.search); + const modelPath = + urlParams.get("filepath") || + // "https://raw.githubusercontent.com/gkjohnson/nasa-urdf-robots/master/r2_description/robots/r2c1.urdf"; + // "https://raw.githubusercontent.com/adubredu/DigitRobot.jl/main/urdf/digit_model.urdf"; + "https://raw.githubusercontent.com/openai/roboschool/1.0.49/roboschool/models_robot/atlas_description/urdf/atlas_v4_with_multisense.urdf"; + // "https://raw.githubusercontent.com/vrtnis/robot-web-viewer/main/public/urdf/robot.urdf"; + // "https://raw.githubusercontent.com/is2ac2/URDF/main/urdf/robot.urdf"; + + const containerStyle = { + width: "50vw", + height: "50vh", + backgroundColor: "#272727", + }; + return ( + + + + + + + + + + + ); +}; diff --git a/frontend/src/components/files/UploadImage.tsx b/frontend/src/components/files/UploadImage.tsx index 650819cc..3a6f857c 100644 --- a/frontend/src/components/files/UploadImage.tsx +++ b/frontend/src/components/files/UploadImage.tsx @@ -1,8 +1,9 @@ import imageCompression from "browser-image-compression"; +import TCButton from "components/files/TCButton"; import { api } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import React, { useState } from "react"; -import { Alert, Button, Col, Form } from "react-bootstrap"; +import { Alert, Col, Form } from "react-bootstrap"; interface ImageUploadProps { onUploadSuccess: (url: string) => void; @@ -82,9 +83,9 @@ const ImageUploadComponent: React.FC = ({ {fileError && {fileError}} - + {uploadStatus && ( { value={newEmail} required /> - + Change Email )} @@ -136,7 +137,7 @@ const Sidebar = ({ show, onHide }: Props) => { value={newPassword} required /> - + Change Password )} diff --git a/frontend/src/hooks/api.tsx b/frontend/src/hooks/api.tsx index bacd9505..43b97940 100644 --- a/frontend/src/hooks/api.tsx +++ b/frontend/src/hooks/api.tsx @@ -18,6 +18,11 @@ export interface Image { url: string; } +export interface Package { + name: string; + url: string; +} + export interface Robot { robot_id: string; name: string; @@ -28,6 +33,8 @@ export interface Robot { height: string; weight: string; degrees_of_freedom: string; + urdf: string; + packages: Package[]; } interface MeResponse { diff --git a/frontend/src/pages/EditRobotForm.tsx b/frontend/src/pages/EditRobotForm.tsx index 787e6b7a..07e7eb05 100644 --- a/frontend/src/pages/EditRobotForm.tsx +++ b/frontend/src/pages/EditRobotForm.tsx @@ -1,7 +1,7 @@ import RobotForm from "components/RobotForm"; import { humanReadableError } from "constants/backend"; import { useAlertQueue } from "hooks/alerts"; -import { api, Bom, Image, Part, Robot } from "hooks/api"; +import { api, Bom, Image, Package, Part, Robot } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import { useTheme } from "hooks/theme"; import React, { FormEvent, useEffect, useState } from "react"; @@ -21,6 +21,8 @@ const EditRobotForm: React.FC = () => { const [robot_description, setDescription] = useState(""); const [robot_height, setHeight] = useState(""); const [robot_weight, setWeight] = useState(""); + const [urdf, setURDF] = useState(""); + const [packages, setPackages] = useState([]); const [robot_degrees_of_freedom, setDof] = useState(""); const [robot_bom, setBom] = useState([]); const [robot_images, setImages] = useState([]); @@ -36,11 +38,13 @@ const EditRobotForm: React.FC = () => { setName(robotData.name); setDescription(robotData.description); setBom(robotData.bom); + setURDF(robotData.urdf); setImages(robotData.images); setRobotId(robotData.robot_id); setHeight(robotData.height); setWeight(robotData.weight); setDof(robotData.degrees_of_freedom); + setPackages(robotData.packages); } catch (err) { addAlert(humanReadableError(err), "error"); } @@ -65,6 +69,8 @@ const EditRobotForm: React.FC = () => { images: robot_images, height: robot_height, weight: robot_weight, + urdf: urdf, + packages: packages, degrees_of_freedom: robot_degrees_of_freedom, }; try { @@ -109,6 +115,10 @@ const EditRobotForm: React.FC = () => { robot_images={robot_images} setImages={setImages} message={message} + robotURDF={urdf} + setURDF={setURDF} + robot_packages={packages} + setPackages={setPackages} /> ); }; diff --git a/frontend/src/pages/Forgot.tsx b/frontend/src/pages/Forgot.tsx index b22ef54f..7c36ef70 100644 --- a/frontend/src/pages/Forgot.tsx +++ b/frontend/src/pages/Forgot.tsx @@ -1,7 +1,8 @@ +import TCButton from "components/files/TCButton"; import { api } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import { FormEvent, useState } from "react"; -import { Button, Form } from "react-bootstrap"; +import { Form } from "react-bootstrap"; const Forgot = () => { const auth = useAuthentication(); @@ -41,9 +42,9 @@ const Forgot = () => { value={email} required /> - + )} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 96b82383..45a27214 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,7 +1,8 @@ +import TCButton from "components/files/TCButton"; import { useAuthentication } from "hooks/auth"; import { useTheme } from "hooks/theme"; import React from "react"; -import { Button, Card, Col, Row } from "react-bootstrap"; +import { Card, Col, Row } from "react-bootstrap"; import { useNavigate } from "react-router-dom"; const Home: React.FC = () => { @@ -37,7 +38,7 @@ const Home: React.FC = () => { <> - + - + - + - + diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index a71c6f6f..41b12779 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,8 +1,9 @@ +import TCButton from "components/files/TCButton"; import { useAlertQueue } from "hooks/alerts"; import { api } from "hooks/api"; import { setLocalStorageAuth, useAuthentication } from "hooks/auth"; import { FormEvent, useState } from "react"; -import { Button, Form } from "react-bootstrap"; +import { Form } from "react-bootstrap"; import { Link, useNavigate } from "react-router-dom"; const Login = () => { @@ -62,7 +63,7 @@ const Login = () => {
Forgot your password?
- + Login ); diff --git a/frontend/src/pages/NewRobot.tsx b/frontend/src/pages/NewRobot.tsx index 87d57af1..ee755dd0 100644 --- a/frontend/src/pages/NewRobot.tsx +++ b/frontend/src/pages/NewRobot.tsx @@ -1,5 +1,5 @@ import RobotForm from "components/RobotForm"; -import { api, Bom, Image, Part, Robot } from "hooks/api"; +import { api, Bom, Image, Package, Part, Robot } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import { useTheme } from "hooks/theme"; import React, { FormEvent, useEffect, useState } from "react"; @@ -13,8 +13,10 @@ const NewRobot: React.FC = () => { const [robot_name, setName] = useState(""); const [robot_height, setHeight] = useState(""); const [robot_weight, setWeight] = useState(""); + const [robot_urdf, setURDF] = useState(""); const [robot_degrees_of_freedom, setDof] = useState(""); const [robot_description, setDescription] = useState(""); + const [robot_packages, setPackages] = useState([]); const [robot_bom, setBom] = useState([]); const [robot_images, setImages] = useState([]); const [parts, setParts] = useState([]); @@ -33,9 +35,11 @@ const NewRobot: React.FC = () => { owner: "", bom: robot_bom, images: robot_images, + urdf: "", height: robot_height, weight: robot_weight, degrees_of_freedom: robot_degrees_of_freedom, + packages: robot_packages, }; try { await auth_api.addRobot(newFormData); @@ -79,6 +83,10 @@ const NewRobot: React.FC = () => { robot_images={robot_images} setImages={setImages} message={message} + setURDF={setURDF} + robotURDF={robot_urdf} + robot_packages={robot_packages} + setPackages={setPackages} /> ); }; diff --git a/frontend/src/pages/PartDetails.tsx b/frontend/src/pages/PartDetails.tsx index fc183f66..916617ac 100644 --- a/frontend/src/pages/PartDetails.tsx +++ b/frontend/src/pages/PartDetails.tsx @@ -1,10 +1,10 @@ +import TCButton from "components/files/TCButton"; import { useAlertQueue } from "hooks/alerts"; import { api, Image } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import { useEffect, useState } from "react"; import { Breadcrumb, - Button, ButtonGroup, Carousel, Col, @@ -40,6 +40,7 @@ const PartDetails = () => { const handleClose = () => setShow(false); + const handleShow = () => setShow(true); const handleShowDelete = () => setShowDelete(true); const handleCloseDelete = () => setShowDelete(false); @@ -150,15 +151,91 @@ const PartDetails = () => { + {part.owner === userId && ( + <> + + + { + navigate(`/edit-part/${id}/`); + }} + > + Edit Part + + + + { + handleShowDelete(); + }} + > + Delete Part + + + + + + + Are you sure you want to delete this part? + + + + { + await auth_api.deletePart(id); + navigate(`/parts/your/1`); + }} + > + Delete Part + + { + handleCloseDelete(); + }} + > + Cancel + + + + + )} + + {images && ( - + 1} > {images.map((image, key) => ( @@ -169,6 +246,9 @@ const PartDetails = () => { justifyContent: "center", overflow: "hidden", }} + onClick={() => { + handleShow(); + }} > @@ -210,101 +290,30 @@ const PartDetails = () => { - - - - + {images.length > 1 && ( + + { + setImageIndex( + (imageIndex - 1 + images.length) % images.length, + ); + }} + > + Previous + + { + setImageIndex((imageIndex + 1) % images.length); + }} + > + Next + + + )} - <> - {part.owner === userId && ( - <> - - - - - - - - - - - - Are you sure you want to delete this part? - - - - - - - - - )} - ); }; diff --git a/frontend/src/pages/Parts.tsx b/frontend/src/pages/Parts.tsx index ff9da5f7..3d8c458d 100644 --- a/frontend/src/pages/Parts.tsx +++ b/frontend/src/pages/Parts.tsx @@ -83,7 +83,7 @@ const Parts = () => { {partsData.map((part) => ( - + navigate(`/part/${part.part_id}`)}> {part.images[0] && (
{ @@ -97,7 +98,7 @@ const Register = () => { value={password} required /> - + Register ) : ( diff --git a/frontend/src/pages/RegistrationEmail.tsx b/frontend/src/pages/RegistrationEmail.tsx index 8c1dde30..ba4d7bb9 100644 --- a/frontend/src/pages/RegistrationEmail.tsx +++ b/frontend/src/pages/RegistrationEmail.tsx @@ -1,8 +1,9 @@ +import TCButton from "components/files/TCButton"; import { useAlertQueue } from "hooks/alerts"; import { api } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import { FormEvent, useState } from "react"; -import { Button, Col, Container, Form, Row, Spinner } from "react-bootstrap"; +import { Col, Container, Form, Row, Spinner } from "react-bootstrap"; const RegistrationEmail = () => { const auth = useAuthentication(); @@ -69,7 +70,7 @@ const RegistrationEmail = () => { value={email} required /> - + Send Code
); diff --git a/frontend/src/pages/ResetPassword.tsx b/frontend/src/pages/ResetPassword.tsx index 86674579..2eaaa665 100644 --- a/frontend/src/pages/ResetPassword.tsx +++ b/frontend/src/pages/ResetPassword.tsx @@ -1,8 +1,9 @@ +import TCButton from "components/files/TCButton"; import { useAlertQueue } from "hooks/alerts"; import { api } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import { FormEvent, useState } from "react"; -import { Button, Form } from "react-bootstrap"; +import { Form } from "react-bootstrap"; import { useParams } from "react-router-dom"; const ResetPassword = () => { @@ -51,9 +52,9 @@ const ResetPassword = () => { value={password} required /> - + )}{" "} diff --git a/frontend/src/pages/RobotDetails.tsx b/frontend/src/pages/RobotDetails.tsx index 97695171..a71c1318 100644 --- a/frontend/src/pages/RobotDetails.tsx +++ b/frontend/src/pages/RobotDetails.tsx @@ -1,11 +1,12 @@ +import { InputerURDFComponent } from "components/files/InputerURDFLoader"; +import TCButton from "components/files/TCButton"; import ImageComponent from "components/files/ViewImage"; import { useAlertQueue } from "hooks/alerts"; -import { api, Bom } from "hooks/api"; +import { api, Bom, Package } from "hooks/api"; import { useAuthentication } from "hooks/auth"; import { useEffect, useState } from "react"; import { Breadcrumb, - Button, ButtonGroup, Carousel, Col, @@ -23,6 +24,8 @@ interface RobotDetailsResponse { owner: string; description: string; images: { url: string; caption: string }[]; + urdf: string; + packages: Package[]; bom: Bom[]; height: string; weight: string; @@ -45,9 +48,11 @@ const RobotDetails = () => { const [ownerUsername, setOwnerUsername] = useState(null); const [robot, setRobot] = useState(null); const [parts, setParts] = useState([]); + const [package_urls, setPackages] = useState([]); const [imageIndex, setImageIndex] = useState(0); const [error, setError] = useState(null); const [showDelete, setShowDelete] = useState(false); + const [isValidURDF, setIsValidURDF] = useState(false); const handleClose = () => setShow(false); const handleShow = () => setShow(true); @@ -62,6 +67,14 @@ const RobotDetails = () => { setRobot(robotData); const ownerUsername = await auth_api.getUserById(robotData.owner); setOwnerUsername(ownerUsername); + const curPackages = []; + for (let i = 0; i < robotData.packages.length; i++) { + const package_id = robotData.packages[i].name; + const package_url = robotData.packages[i].url; + curPackages.push(package_id); + curPackages.push(package_url); + } + setPackages(curPackages); const parts = robotData.bom.map(async (part) => { return { name: (await auth_api.getPartById(part.part_id)).name, @@ -74,6 +87,24 @@ const RobotDetails = () => { .filter(isFulfilled) .map((result) => result.value as ExtendedBom), ); + if (robotData.urdf) { + try { + const response = await fetch(robotData.urdf, { + method: "HEAD", + headers: { + Accept: "application/vnd.github.v3.raw", + }, + }); + if (response.ok) { + setIsValidURDF(true); + } else { + throw new Error("Invalid URDF URL"); + } + } catch (err) { + setIsValidURDF(false); + console.error("URDF validation error: ", err); + } + } } catch (err) { if (err instanceof Error) { setError(err.message); @@ -84,6 +115,7 @@ const RobotDetails = () => { }; fetchRobot(); }, [id]); + // }); useEffect(() => { if (auth.isAuthenticated) { try { @@ -133,6 +165,8 @@ const RobotDetails = () => { bom: robot?.bom, height: robot?.height, weight: robot?.weight, + urdf: robot?.urdf, + packages: robot?.packages, degrees_of_freedom: robot?.degrees_of_freedom, }; @@ -226,52 +260,141 @@ const RobotDetails = () => {
- - - {images && ( - - { - setImageIndex(0); - handleShow(); - }} - > - {images.map((image, key) => ( - -
+ + + { + navigate(`/edit-robot/${id}/`); }} > - -
- + + + { + handleShowDelete(); }} > - {image.caption} - -
- ))} -
- - )} + Delete Robot + + + + + + + Are you sure you want to delete this robot? + + + + { + await auth_api.deleteRobot(id); + navigate(`/robots/your/1`); + }} + > + Delete Robot + + { + handleCloseDelete(); + }} + > + Cancel + + + + + )} + + + + {isValidURDF && ( + + + + {/* */} + + )} + {images && ( + + 1} + > + {images.map((image, key) => ( + +
{ + handleShow(); + }} + > + +
+ + {image.caption} + +
+ ))} +
+
+ )} + { > - {images[imageIndex].caption} ({imageIndex + 1} of {images.length}{" "} - {userId} + {images[imageIndex].caption} ({imageIndex + 1} of {images.length}) @@ -303,101 +425,30 @@ const RobotDetails = () => { - - - - + {images.length > 1 && ( + + { + setImageIndex( + (imageIndex - 1 + images.length) % images.length, + ); + }} + > + Previous + + { + setImageIndex((imageIndex + 1) % images.length); + }} + > + Next + + + )} - <> - {robot.owner === userId && ( - <> - - - - - - - - - - - - Are you sure you want to delete this robot? - - - - - - - - - )} - ); }; diff --git a/frontend/src/pages/Robots.tsx b/frontend/src/pages/Robots.tsx index b0a15a1c..0b51b173 100644 --- a/frontend/src/pages/Robots.tsx +++ b/frontend/src/pages/Robots.tsx @@ -17,7 +17,7 @@ import { Link, useNavigate, useParams } from "react-router-dom"; const Robots = () => { const auth = useAuthentication(); const auth_api = new api(auth.api); - const [robotsData, setRobot] = useState(null); + const [robotsData, setRobot] = useState([]); const [moreRobots, setMoreRobots] = useState(false); const [idMap, setIdMap] = useState>(new Map()); const { addAlert } = useAlertQueue(); diff --git a/frontend/src/pages/TestImages.tsx b/frontend/src/pages/TestImages.tsx index 66d8492e..7f4be8a5 100644 --- a/frontend/src/pages/TestImages.tsx +++ b/frontend/src/pages/TestImages.tsx @@ -1,13 +1,11 @@ +import { URDFComponent } from "components/files/URDFLoader"; import React from "react"; const App: React.FC = () => { return (

Robot Images

- {/* - - */} - {/* */} +
); }; diff --git a/frontend/src/pages/YourParts.tsx b/frontend/src/pages/YourParts.tsx index 197b35ca..b97e1553 100644 --- a/frontend/src/pages/YourParts.tsx +++ b/frontend/src/pages/YourParts.tsx @@ -75,7 +75,7 @@ const YourParts = () => { {partsData.map((part) => ( - + navigate(`/part/${part.part_id}`)}> {part.images[0] && (
{ const auth = useAuthentication(); const auth_api = new api(auth.api); - const [robotsData, setRobot] = useState(null); + const [robotsData, setRobot] = useState([]); const [moreRobots, setMoreRobots] = useState(false); const { addAlert } = useAlertQueue(); diff --git a/store/app/crud/robots.py b/store/app/crud/robots.py index 7a06cc43..4845b831 100644 --- a/store/app/crud/robots.py +++ b/store/app/crud/robots.py @@ -9,7 +9,7 @@ from pydantic import BaseModel from store.app.crud.base import BaseCrud -from store.app.model import Bom, Image, Part, Robot +from store.app.model import Bom, Image, Package, Part, Robot from store.settings import settings logger = logging.getLogger(__name__) @@ -29,6 +29,8 @@ class EditRobot(BaseModel): height: Optional[str] weight: Optional[str] degrees_of_freedom: Optional[str] + urdf: Optional[str] + packages: List[Package] def serialize_bom(bom: Bom) -> dict: @@ -47,6 +49,14 @@ def serialize_image_list(image_list: List[Image]) -> List[dict]: return [serialize_image(image) for image in image_list] +def serialize_package(package: Package) -> dict: + return {"name": package.name, "url": package.url} + + +def serialize_package_list(package_list: List[Package]) -> List[dict]: + return [serialize_package(package) for package in package_list] + + def get_timestamp(item: Dict[str, Any]) -> int: return item["timestamp"] @@ -60,7 +70,7 @@ async def add_part(self, part: Part) -> None: table = await self.db.Table("Parts") await table.put_item(Item=part.model_dump()) - async def list_robots(self, page: int = 1, items_per_page: int = 18) -> tuple[list[Robot], bool]: + async def list_robots(self, page: int = 1, items_per_page: int = 12) -> tuple[list[Robot], bool]: table = await self.db.Table("Robots") response = await table.scan() # This is O(n log n). Look into better ways to architect the schema. @@ -69,7 +79,7 @@ async def list_robots(self, page: int = 1, items_per_page: int = 18) -> tuple[li Robot.model_validate(item) for item in sorted_items[(page - 1) * items_per_page : page * items_per_page] ], page * items_per_page < response["Count"] - async def list_your_robots(self, user_id: str, page: int = 1, items_per_page: int = 18) -> tuple[list[Robot], bool]: + async def list_your_robots(self, user_id: str, page: int = 1, items_per_page: int = 12) -> tuple[list[Robot], bool]: table = await self.db.Table("Robots") response = await table.query(IndexName="ownerIndex", KeyConditionExpression=Key("owner").eq(user_id)) sorted_items = sorted(response["Items"], key=get_timestamp, reverse=True) @@ -84,7 +94,7 @@ async def get_robot(self, robot_id: str) -> Robot | None: return None return Robot.model_validate(robot_dict["Item"]) - async def list_parts(self, page: int = 1, items_per_page: int = 18) -> tuple[list[Part], bool]: + async def list_parts(self, page: int = 1, items_per_page: int = 12) -> tuple[list[Part], bool]: table = await self.db.Table("Parts") response = await table.scan() # This is O(n log n). Look into better ways to architect the schema. @@ -98,7 +108,7 @@ async def dump_parts(self) -> list[Part]: response = await table.scan() return [Part.model_validate(item) for item in response["Items"]] - async def list_your_parts(self, user_id: str, page: int = 1, items_per_page: int = 18) -> tuple[list[Part], bool]: + async def list_your_parts(self, user_id: str, page: int = 1, items_per_page: int = 12) -> tuple[list[Part], bool]: table = await self.db.Table("Parts") response = await table.query(IndexName="ownerIndex", KeyConditionExpression=Key("owner").eq(user_id)) sorted_items = sorted(response["Items"], key=get_timestamp, reverse=True) @@ -152,13 +162,15 @@ async def update_robot(self, id: str, robot: EditRobot) -> None: update_expression = "SET #name = :name, \ #description = :description, \ #bom = :bom, \ - #images = :images, " + #images = :images, \ + #packages = :packages, " expression_attribute_names = { "#name": "name", "#description": "description", "#bom": "bom", "#images": "images", + "#packages": "packages", } expression_attribute_values = { @@ -166,22 +178,28 @@ async def update_robot(self, id: str, robot: EditRobot) -> None: ":description": robot.description, ":bom": serialize_bom_list(robot.bom), ":images": serialize_image_list(robot.images), + ":packages": serialize_package_list(robot.packages), } - if robot.height: + if robot.urdf is not None: + update_expression += "#urdf = :urdf, " + expression_attribute_names["#urdf"] = "urdf" + expression_attribute_values[":urdf"] = robot.urdf or "" + + if robot.height is not None: update_expression += "#height = :height, " expression_attribute_names["#height"] = "height" - expression_attribute_values[":height"] = robot.height + expression_attribute_values[":height"] = robot.height or "" - if robot.weight: + if robot.weight is not None: update_expression += "#weight = :weight, " expression_attribute_names["#weight"] = "weight" - expression_attribute_values[":weight"] = robot.weight + expression_attribute_values[":weight"] = robot.weight or "" - if robot.degrees_of_freedom: + if robot.degrees_of_freedom is not None: update_expression += "#degrees_of_freedom = :degrees_of_freedom, " expression_attribute_names["#degrees_of_freedom"] = "degrees_of_freedom" - expression_attribute_values[":degrees_of_freedom"] = robot.degrees_of_freedom + expression_attribute_values[":degrees_of_freedom"] = robot.degrees_of_freedom or "" await table.update_item( Key={"robot_id": id}, diff --git a/store/app/model.py b/store/app/model.py index a4955d22..9c963093 100644 --- a/store/app/model.py +++ b/store/app/model.py @@ -41,6 +41,11 @@ class Image(BaseModel): url: str +class Package(BaseModel): + name: str + url: str + + class Robot(BaseModel): robot_id: str # Primary key owner: str @@ -52,6 +57,8 @@ class Robot(BaseModel): weight: Optional[str] = "" degrees_of_freedom: Optional[str] = "" timestamp: int + urdf: str + packages: list[Package] class Part(BaseModel): diff --git a/store/app/routers/robot.py b/store/app/routers/robot.py index c2a36fd4..c1f27953 100644 --- a/store/app/routers/robot.py +++ b/store/app/routers/robot.py @@ -10,7 +10,7 @@ from store.app.crud.robots import EditRobot from store.app.crypto import new_uuid from store.app.db import Crud -from store.app.model import Bom, Image, Robot +from store.app.model import Bom, Image, Package, Robot from store.app.routers.users import get_session_token robots_router = APIRouter() @@ -25,7 +25,7 @@ async def list_robots( ) -> tuple[List[Robot], bool]: """Lists the robots in the database. - The function is paginated. The page size is 18. + The function is paginated. The page size is 12. Returns the robots on the page and a boolean indicating if there are more pages. """ @@ -67,6 +67,8 @@ class NewRobot(BaseModel): height: Optional[str] weight: Optional[str] degrees_of_freedom: Optional[str] + urdf: str + packages: List[Package] @robots_router.post("/add/") @@ -91,6 +93,8 @@ async def add_robot( owner=str(user_id), robot_id=str(new_uuid()), timestamp=int(time.time()), + urdf=new_robot.urdf, + packages=new_robot.packages, ) ) return True