From e54e3b2b2742ebaa9aef3e163250b1cf80b1b6b9 Mon Sep 17 00:00:00 2001 From: Ben Bolte Date: Sat, 10 Aug 2024 01:50:06 -0700 Subject: [PATCH] urdf uploading (#283) * urdf uploading * urdf uploading + cleanup * stuff * lint * lint --- frontend/package-lock.json | 10 ++ frontend/package.json | 1 + frontend/prettier.config.js | 1 - frontend/src/components/Editor.tsx | 51 ------- .../{Image.tsx => ImagePlaceholder.tsx} | 4 +- frontend/src/components/MultiLeva.tsx | 50 ------- frontend/src/components/Stls.tsx | 41 ------ .../components/listing/ListingArtifacts.tsx | 6 + .../src/components/listing/ListingSTLs.tsx | 90 ++++-------- .../src/components/listing/ListingURDFs.tsx | 134 ++++++++++++++++++ .../{ => listing/renderers}/Loader.tsx | 0 .../listing/renderers/StlRenderer.tsx | 106 ++++++++++++++ .../listing/renderers/UrdfRenderer.tsx | 121 ++++++++++++++++ .../components/listings/ListingGridCard.tsx | 4 +- frontend/src/hooks/useAlertQueue.tsx | 5 +- store/app/model.py | 6 +- tests/test_listings.py | 5 + 17 files changed, 423 insertions(+), 212 deletions(-) delete mode 100644 frontend/src/components/Editor.tsx rename frontend/src/components/{Image.tsx => ImagePlaceholder.tsx} (62%) delete mode 100644 frontend/src/components/MultiLeva.tsx delete mode 100644 frontend/src/components/Stls.tsx create mode 100644 frontend/src/components/listing/ListingURDFs.tsx rename frontend/src/components/{ => listing/renderers}/Loader.tsx (100%) create mode 100644 frontend/src/components/listing/renderers/StlRenderer.tsx create mode 100644 frontend/src/components/listing/renderers/UrdfRenderer.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1255737..14ea7609 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -33,6 +33,7 @@ "remark-gfm": "^4.0.0", "tailwind-merge": "^2.4.0", "three": "^0.167.1", + "urdf-loader": "^0.12.1", "zod": "^3.23.8", "zxcvbn": "^4.4.2" }, @@ -10215,6 +10216,15 @@ "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==", + "license": "Apache-2.0", + "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", diff --git a/frontend/package.json b/frontend/package.json index 18589a8d..028900e3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,6 +48,7 @@ "remark-gfm": "^4.0.0", "tailwind-merge": "^2.4.0", "three": "^0.167.1", + "urdf-loader": "^0.12.1", "zod": "^3.23.8", "zxcvbn": "^4.4.2" }, diff --git a/frontend/prettier.config.js b/frontend/prettier.config.js index b5ce0411..38883df7 100644 --- a/frontend/prettier.config.js +++ b/frontend/prettier.config.js @@ -3,7 +3,6 @@ module.exports = { tabWidth: 2, trailingComma: "all", singleQuote: false, - jsxBracketSameLine: true, semi: true, plugins: ["@trivago/prettier-plugin-sort-imports"], importOrder: [ diff --git a/frontend/src/components/Editor.tsx b/frontend/src/components/Editor.tsx deleted file mode 100644 index 5b4eb037..00000000 --- a/frontend/src/components/Editor.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ -// @ts-nocheck - -/* eslint-disable react/no-unknown-property */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Dispatch, FC, SetStateAction, Suspense, useRef } from "react"; - -import { Center, Select } from "@react-three/drei"; -import { useLoader } from "@react-three/fiber"; -import { Object3D } from "three"; -import { STLLoader } from "three/examples/jsm/loaders/STLLoader"; - -import Loader from "./Loader"; -import Stls from "./Stls"; - -const files = ["bone", "heart", "LLL"]; -const color = ["#9c9ea1", "#781e14", "#d66154"]; -const opacity = [1, 1, 1]; - -interface Props { - setSelected: Dispatch>; - url: string; -} - -const Editor: FC = ({ setSelected, url }) => { - const stl = useLoader(STLLoader, [url]); - const group = useRef(null!); - - return ( - }> -
- -
-
- ); -}; - -export default Editor; diff --git a/frontend/src/components/Image.tsx b/frontend/src/components/ImagePlaceholder.tsx similarity index 62% rename from frontend/src/components/Image.tsx rename to frontend/src/components/ImagePlaceholder.tsx index 170db31b..b150f764 100644 --- a/frontend/src/components/Image.tsx +++ b/frontend/src/components/ImagePlaceholder.tsx @@ -1,7 +1,7 @@ -const Image = () => { +const ImagePlaceholder = () => { return (
); }; -export default Image; +export default ImagePlaceholder; diff --git a/frontend/src/components/MultiLeva.tsx b/frontend/src/components/MultiLeva.tsx deleted file mode 100644 index e735cfff..00000000 --- a/frontend/src/components/MultiLeva.tsx +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ -// @ts-nocheck -import { FC } from "react"; - -import { - LevaPanel, - useControls as useControlsImpl, - useCreateStore, -} from "leva"; -import { Object3D } from "three"; - -interface Props { - selected: Object3D[] | undefined; -} - -export const Panel: FC = ({ selected }) => { - if (selected) { - return ; - } - - return ; -}; - -export const useControls = (selected, props) => { - const store = useCreateStore(); - const isFirst = selected[0] === store; - const materialProps = useControlsImpl( - Object.keys(props).reduce( - (acc, key) => ({ - ...acc, - [key]: { - ...props[key], - transient: false, - onChange: (value, path, ctx) => - !ctx.initial && - isFirst && - selected.length > 1 && - selected.forEach((s, i) => i > 0 && s.setValueAtPath(path, value)), - render: () => - selected.length === 1 || - selected.every((store) => store.getData()[key]), - }, - }), - {}, - ), - { store }, - [selected], - ); - return [store, materialProps]; -}; diff --git a/frontend/src/components/Stls.tsx b/frontend/src/components/Stls.tsx deleted file mode 100644 index 5513e0e3..00000000 --- a/frontend/src/components/Stls.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ -// @ts-nocheck - -/* eslint-disable react/no-unknown-property */ - -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { FC } from "react"; - -import { Edges, useSelect } from "@react-three/drei"; - -import { useControls } from "components/MultiLeva"; - -interface Props { - stl: any; - organName: string; - color: string; - opacity: number; -} - -const Stls: FC = ({ organName, color, opacity, stl }) => { - const selected = useSelect().map((sel) => sel.userData.store); - const [store, materialProps] = useControls(selected, { - name: { value: organName }, - color: { value: color }, - opacity: { value: opacity, min: 0.2, max: 1, step: 0.1 }, - visible: { value: true }, - }); - const isSelected = !!selected.find((sel) => sel === store); - - return ( - - - - - - - - ); -}; - -export default Stls; diff --git a/frontend/src/components/listing/ListingArtifacts.tsx b/frontend/src/components/listing/ListingArtifacts.tsx index c69d9a4e..36778f9c 100644 --- a/frontend/src/components/listing/ListingArtifacts.tsx +++ b/frontend/src/components/listing/ListingArtifacts.tsx @@ -8,6 +8,7 @@ import Spinner from "components/ui/Spinner"; import ListingImages from "./ListingImages"; import ListingSTLs from "./ListingSTLs"; +import ListingURDFs from "./ListingURDFs"; interface Props { listingId: string; @@ -56,6 +57,11 @@ const ListingArtifacts = (props: Props) => { edit={edit} allArtifacts={artifacts} /> +
); diff --git a/frontend/src/components/listing/ListingSTLs.tsx b/frontend/src/components/listing/ListingSTLs.tsx index 0c01ee83..51abab27 100644 --- a/frontend/src/components/listing/ListingSTLs.tsx +++ b/frontend/src/components/listing/ListingSTLs.tsx @@ -1,55 +1,15 @@ -/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ -// @ts-nocheck +import { useEffect, useState } from "react"; +import { FaCaretSquareDown, FaCaretSquareUp } from "react-icons/fa"; -/* eslint-disable react/no-unknown-property */ -import { Suspense, useState } from "react"; -import { FaCaretSquareDown, FaCaretSquareUp, FaTimes } from "react-icons/fa"; - -import { OrbitControls, PerspectiveCamera } from "@react-three/drei"; -import { Canvas } from "@react-three/fiber"; import { cx } from "class-variance-authority"; import { components } from "gen/api"; import { useAlertQueue } from "hooks/useAlertQueue"; import { useAuthentication } from "hooks/useAuth"; -import { Object3D } from "three"; -import Editor from "components/Editor"; -import Loader from "components/Loader"; -import { Panel } from "components/MultiLeva"; import ListingFileUpload from "components/listing/ListingFileUpload"; +import StlRenderer from "components/listing/renderers/StlRenderer"; import { Button } from "components/ui/Button/Button"; -interface SingleStlViewerProps { - url: string; -} - -const SingleStlViewer = (props: SingleStlViewerProps) => { - const { url } = props; - const [selected, setSelected] = useState(); - - return ( - <> - - }> - - - - - - - - - - ); -}; - interface Props { listingId: string; edit: boolean; @@ -65,12 +25,24 @@ const ListingSTLs = (props: Props) => { const [stls, setStls] = useState< components["schemas"]["ListArtifactsResponse"]["artifacts"] >(allArtifacts.filter((a) => a.artifact_type === "stl")); + const [stl, setStl] = useState< + components["schemas"]["ListArtifactsResponse"]["artifacts"][0] | null + >(null); const [deletingIds, setDeletingIds] = useState([]); const [collapsed, setCollapsed] = useState(true); + const [currentId, setCurrentId] = useState(0); + + useEffect(() => { + if (stl !== null) { + return; + } - const [currentIdUnchecked, setCurrentId] = useState(0); - const currentId = Math.min(Math.max(currentIdUnchecked, 0), stls.length - 1); - const stl = stls.length === 0 ? null : stls[currentId]; + if (currentId >= stls.length || currentId < 0) { + setCurrentId(Math.min(Math.max(currentId, 0), stls.length - 1)); + } else { + setStl(stls[currentId]); + } + }, [stl, stls, currentId]); const onDelete = async (stlId: string) => { setDeletingIds([...deletingIds, stlId]); @@ -87,10 +59,8 @@ const ListingSTLs = (props: Props) => { if (error) { addErrorAlert(error); } else { - if (currentId >= stls.length) { - setCurrentId(stls.length - 1); - } setStls(stls.filter((stl) => stl.artifact_id !== stlId)); + setStl(null); setDeletingIds(deletingIds.filter((id) => id !== stlId)); } }; @@ -126,7 +96,10 @@ const ListingSTLs = (props: Props) => { "px-4 py-2 text-sm font-medium border-t border-b border-gray-200 hover:bg-gray-100", )} key={stl.artifact_id} - onClick={() => setCurrentId(idx)} + onClick={() => { + setCurrentId(idx); + setStl(null); + }} > {idx + 1} @@ -139,17 +112,12 @@ const ListingSTLs = (props: Props) => { key={stl.artifact_id} className="bg-background rounded-lg p-2 relative" > - - {edit && ( - - )} + onDelete(stl.artifact_id)} + disabled={deletingIds.includes(stl.artifact_id)} + /> )} diff --git a/frontend/src/components/listing/ListingURDFs.tsx b/frontend/src/components/listing/ListingURDFs.tsx new file mode 100644 index 00000000..1ddf45be --- /dev/null +++ b/frontend/src/components/listing/ListingURDFs.tsx @@ -0,0 +1,134 @@ +import { useState } from "react"; +import { FaCaretSquareDown, FaCaretSquareUp } from "react-icons/fa"; + +import { cx } from "class-variance-authority"; +import { components } from "gen/api"; +import { useAlertQueue } from "hooks/useAlertQueue"; +import { useAuthentication } from "hooks/useAuth"; + +import ListingFileUpload from "components/listing/ListingFileUpload"; +import UrdfRenderer from "components/listing/renderers/UrdfRenderer"; +import { Button } from "components/ui/Button/Button"; + +interface Props { + listingId: string; + edit: boolean; + allArtifacts: components["schemas"]["ListArtifactsResponse"]["artifacts"]; +} + +const ListingURDFs = (props: Props) => { + const { listingId, edit, allArtifacts } = props; + + const auth = useAuthentication(); + const { addErrorAlert } = useAlertQueue(); + + const [urdfs, setUrdfs] = useState< + components["schemas"]["ListArtifactsResponse"]["artifacts"] + >(allArtifacts.filter((a) => a.artifact_type === "urdf")); + const [deletingIds, setDeletingIds] = useState([]); + const [collapsed, setCollapsed] = useState(false); + + const [currentIdUnchecked, setCurrentId] = useState(0); + const currentId = Math.min(Math.max(currentIdUnchecked, 0), urdfs.length - 1); + const urdf = urdfs.length === 0 ? null : urdfs[currentId]; + + const onDelete = async (urdfId: string) => { + setDeletingIds([...deletingIds, urdfId]); + + const { error } = await auth.client.DELETE( + "/artifacts/delete/{artifact_id}", + { + params: { + path: { artifact_id: urdfId }, + }, + }, + ); + + if (error) { + addErrorAlert(error); + } else { + if (currentId >= urdfs.length) { + setCurrentId(urdfs.length - 1); + } + setUrdfs(urdfs.filter((urdf) => urdf.artifact_id !== urdfId)); + setDeletingIds(deletingIds.filter((id) => id !== urdfId)); + } + }; + + return urdf !== null || edit ? ( +
+ {urdf !== null ? ( + <> + + {!collapsed && urdfs.length > 1 && ( +
+ {urdfs.map((urdf, idx) => ( + + ))} +
+ )} + {!collapsed && ( +
+
+ onDelete(urdf.artifact_id)} + disabled={deletingIds.includes(urdf.artifact_id)} + /> +
+
+ )} + + ) : ( +

+ URDFs +

+ )} + {edit && ( + { + setUrdfs([...urdfs, artifact.artifact]); + }} + /> + )} +
+ ) : ( + <> + ); +}; + +export default ListingURDFs; diff --git a/frontend/src/components/Loader.tsx b/frontend/src/components/listing/renderers/Loader.tsx similarity index 100% rename from frontend/src/components/Loader.tsx rename to frontend/src/components/listing/renderers/Loader.tsx diff --git a/frontend/src/components/listing/renderers/StlRenderer.tsx b/frontend/src/components/listing/renderers/StlRenderer.tsx new file mode 100644 index 00000000..ffdf7436 --- /dev/null +++ b/frontend/src/components/listing/renderers/StlRenderer.tsx @@ -0,0 +1,106 @@ +/* eslint-disable react/no-unknown-property */ +import { Suspense, useState } from "react"; +import { FaTimes } from "react-icons/fa"; + +import { Center, OrbitControls, PerspectiveCamera } from "@react-three/drei"; +import { Canvas } from "@react-three/fiber"; +import { useLoader } from "@react-three/fiber"; +import { STLLoader } from "three/examples/jsm/loaders/STLLoader"; + +import Loader from "components/listing/renderers/Loader"; +import { Button } from "components/ui/Button/Button"; + +type MeshType = "wireframe" | "basic"; + +const MeshTypes: MeshType[] = ["wireframe", "basic"]; + +interface ModelProps { + url: string; + meshType: MeshType; +} + +const getMaterial = (meshType: MeshType) => { + switch (meshType) { + case "wireframe": + return ; + case "basic": + default: + return ( + + ); + } +}; + +const Model = ({ url, meshType }: ModelProps) => { + const geom = useLoader(STLLoader, url); + + return ( + + + {getMaterial(meshType)} + + ); +}; + +interface Props { + url: string; + edit?: boolean; + onDelete?: () => void; + disabled?: boolean; +} + +const StlRenderer = ({ url, edit, onDelete, disabled }: Props) => { + const [meshType, setMeshType] = useState("basic"); + + return ( + <> + + + + + + }> +
+ +
+
+
+ {edit && ( + + )} + + + ); +}; + +export default StlRenderer; diff --git a/frontend/src/components/listing/renderers/UrdfRenderer.tsx b/frontend/src/components/listing/renderers/UrdfRenderer.tsx new file mode 100644 index 00000000..4516a980 --- /dev/null +++ b/frontend/src/components/listing/renderers/UrdfRenderer.tsx @@ -0,0 +1,121 @@ +/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ +// @ts-nocheck + +/* eslint-disable react/no-unknown-property */ +import { Suspense, useRef, useState } from "react"; +import { FaTimes } from "react-icons/fa"; + +import { + Center, + OrbitControls, + PerspectiveCamera, + Plane, +} from "@react-three/drei"; +import { Canvas } from "@react-three/fiber"; +import { useLoader } from "@react-three/fiber"; +import { Group } from "three"; +import URDFLoader from "urdf-loader"; + +import Loader from "components/listing/renderers/Loader"; +import { Button } from "components/ui/Button/Button"; + +type MeshType = "wireframe" | "basic"; + +const MeshTypes: MeshType[] = ["wireframe", "basic"]; + +interface ModelProps { + url: string; + meshType: MeshType; +} + +const Model = ({ url, meshType }: ModelProps) => { + // TODO: Go back to using URL. + const filepath = + "https://raw.githubusercontent.com/facebookresearch/fairo/main/polymetis/polymetis/data/franka_panda/panda_arm.urdf"; + + console.log(url, meshType); + + const ref = useRef(); + const robot = useLoader(URDFLoader, filepath); + + return ( + + + + + + + + + ); +}; + +interface Props { + url: string; + edit?: boolean; + onDelete?: () => void; + disabled?: boolean; +} + +const UrdfRenderer = ({ url, edit, onDelete, disabled }: Props) => { + const [meshType, setMeshType] = useState("basic"); + + return ( + <> + + + + + + }> +
+ +
+
+
+ {edit && ( + + )} + + + ); +}; + +export default UrdfRenderer; diff --git a/frontend/src/components/listings/ListingGridCard.tsx b/frontend/src/components/listings/ListingGridCard.tsx index 085244a7..a0d15d5d 100644 --- a/frontend/src/components/listings/ListingGridCard.tsx +++ b/frontend/src/components/listings/ListingGridCard.tsx @@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom"; import clsx from "clsx"; import { paths } from "gen/api"; -import Image from "components/Image"; +import ImagePlaceholder from "components/ImagePlaceholder"; import { RenderDescription } from "components/listing/ListingDescription"; import { Card, CardContent, CardHeader, CardTitle } from "components/ui/Card"; @@ -44,7 +44,7 @@ const ListingGridCard = (props: Props) => { /> ) : ( - + )}
diff --git a/frontend/src/hooks/useAlertQueue.tsx b/frontend/src/hooks/useAlertQueue.tsx index 009f0c13..18e37204 100644 --- a/frontend/src/hooks/useAlertQueue.tsx +++ b/frontend/src/hooks/useAlertQueue.tsx @@ -8,7 +8,7 @@ import { import Toast from "components/ui/Toast"; -const DELAY = 3000; +const DELAY = 5000; const MAX_ALERTS = 5; // eslint-disable-next-line @@ -19,6 +19,9 @@ export const humanReadableError = (error: any | undefined) => { if (error?.message) { return error.message; } + if (error?.detail) { + return error.detail; + } return "An unknown error occurred"; }; diff --git a/store/app/model.py b/store/app/model.py index 5f77ecd0..e9021fea 100644 --- a/store/app/model.py +++ b/store/app/model.py @@ -119,10 +119,10 @@ def create(cls, user_id: str, source: APIKeySource, permissions: APIKeyPermissio # Image "image": {"image/png", "image/jpeg", "image/jpg", "image/gif", "image/webp"}, # XML - "urdf": {"application/xml"}, - "mjcf": {"application/xml"}, + "urdf": {"application/octet-stream", "text/xml", "application/xml"}, + "mjcf": {"application/octet-stream", "text/xml", "application/xml"}, # Binary or text - "stl": {"application/octet-stream", "text/xml"}, + "stl": {"application/octet-stream", "text/plain"}, } DOWNLOAD_CONTENT_TYPE: dict[ArtifactType, str] = { diff --git a/tests/test_listings.py b/tests/test_listings.py index 55961f8b..2251d211 100644 --- a/tests/test_listings.py +++ b/tests/test_listings.py @@ -58,6 +58,11 @@ async def test_listings(app_client: AsyncClient, tmpdir: Path) -> None: data = response.json() assert data["artifact"]["artifact_id"] is not None + # Gets the URDF URL. + artifact_id = data["artifact"]["artifact_id"] + response = await app_client.get(f"/artifacts/url/urdf/{artifact_id}", headers=auth_headers) + assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT, response.content + # Uploads an STL. stl_path = Path(__file__).parent / "assets" / "teapot.stl" data_json = json.dumps({"artifact_type": "stl", "listing_id": listing_id})