diff --git a/CHANGELOG.md b/CHANGELOG.md index d867931..9dcd345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,14 @@ All notable changes to this project will be documented in this file. - Description: AgileX PiPER (URDF) - Description: Robot Soccer Kit - Description: Robotiq 2F-85 (MJCF V4) (thanks to @peterdavidfagan) +- CLI: Add `show_in_meshcat` command +- CLI: Add `show_in_mujoco` command +- CLI: Add `show_in_pybullet` command + +### Changed + +- Enable command-line to run from `python -m robot_descriptions` +- CLI: Rename `show` command to `show_in_yourdfpy` ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e24c5a4..956f734 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,19 +7,19 @@ The goal of this project is to facilitate loading and sharing of robot descripti - [Add a new robot description](#adding-a-new-robot-description) - Raise any issue you find in a description, preferably directly in the description's repository (check out [`_repositories.py`](https://github.com/stephane-caron/robot_descriptions.py/blob/main/robot_descriptions/_repositories.py)) -- Make a standalone ``_description`` repository for a description embedded in one of the big framework repositories (Bullet, Drake, ...) +- Make a standalone `_description` repository for a description embedded in one of the big framework repositories (Bullet, Drake, ...) ## Adding a new robot description 1. **License:** The robot description is distributed legally and under an open source license (permissive or copyleft). -2. **Repository:** If needed, add the repository containing the new description to ``_repositories.py``. +2. **Repository:** If needed, add the repository containing the new description to `_repositories.py`. - Use a specific git commit ID. This way the robot description will still work in the interval between a change in the file structure of the target repository and the corresponding update in `robot_descriptions`. -3. **Submodule:** add a Python file for the robot descriptions to ``robot_descriptions/``. - - The file name for the new submodule is ``_description.py`` in snake-case. - - For example, the file name for the Kinova (maker) Gen2 (robot name) is ``gen2_description.py``. - - Use the ``mj_description`` suffix for an MJCF description. -4. **Listing:** Add the description metadata to the ``DESCRIPTIONS`` dictionary in ``_descriptions.py``. +3. **Submodule:** add a Python file for the robot descriptions to `robot_descriptions/`. + - The file name for the new submodule is `_description.py` in snake-case. + - For example, the file name for the Kinova (maker) Gen2 (robot name) is `gen2_description.py`. + - Use the `mj_description` suffix for an MJCF description. +4. **Listing:** Add the description metadata to the `DESCRIPTIONS` dictionary in `_descriptions.py`. 5. **README:** Document the description's submodule name in the Descriptions section of the [README](README.md). - Use an [SPDX License Identifier](https://spdx.org/licenses/) in the License column. 6. **CHANGELOG:** Write down the new model at the top of the [changelog](CHANGELOG.md). -7. **Testing:** Check that all unit tests are successful by ``tox``. +7. **Testing:** Check that all unit tests are successful by `tox`. diff --git a/README.md b/README.md index 3d34797..bb8d4b0 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,19 @@ Loaders are implemented for the following robotics software: Loading will automatically download the robot description if needed, and cache it to a local directory. +### Show a description + +You can display a robot description directly from the command line: + +```console +python -m robot_descriptions show_in_meshcat go2_description +``` + +A `robot_descriptions` alias for `python -m robot_descriptions` is also available. + ### Import as submodule -You can also import a robot description directly as a submodule of ``robot_descriptions``: +You can also import a robot description directly as a submodule of `robot_descriptions`: ```python from robot_descriptions import my_robot_description @@ -83,7 +93,7 @@ The import will automatically download the robot description if you don't have i -Some robot descriptions include additional fields. For instance, the ``iiwa14_description`` exports ``URDF_PATH_POLYTOPE_COLLISION`` with more detailed collision meshes. +Some robot descriptions include additional fields. For instance, the `iiwa14_description` exports `URDF_PATH_POLYTOPE_COLLISION` with more detailed collision meshes. ## Examples @@ -104,14 +114,6 @@ Visualizing a robot description: - [RoboMeshCat](https://github.com/robot-descriptions/robot_descriptions.py/tree/main/examples/show_in_robomeshcat.py) - [yourdfpy](https://github.com/robot-descriptions/robot_descriptions.py/tree/main/examples/show_in_yourdfpy.py) -## Command line tool - -The command line tool can be used to visualize any of the robot descriptions below. For example: - -```console -robot_descriptions show solo_description -``` - ## Descriptions Available robot descriptions ([gallery](https://github.com/robot-descriptions/awesome-robot-descriptions#gallery)) are listed in the following categories: diff --git a/examples/show_in_meshcat.py b/examples/show_in_meshcat.py index 5218956..88f763f 100644 --- a/examples/show_in_meshcat.py +++ b/examples/show_in_meshcat.py @@ -7,8 +7,9 @@ """ Show a robot descriptions, specified from the command line, using MeshCat. -This example requires Pinocchio, installed by e.g. `conda install pinocchio`, -and MeshCat, which is installed by `pip install meshcat`. +This example is equivalent to `python -m robot_descriptions show_in_meshcat`. +It requires Pinocchio, installed by e.g. `conda install pinocchio`, and +MeshCat, installed by e.g. `conda install meshcat-python`. """ import argparse @@ -16,7 +17,7 @@ try: from pinocchio.visualize import MeshcatVisualizer except ImportError as e: - raise ImportError("Pinocchio not found, try ``pip install pin``") from e + raise ImportError("Pinocchio not found, try `pip install pin`") from e from robot_descriptions.loaders.pinocchio import load_robot_description diff --git a/examples/show_in_mujoco.py b/examples/show_in_mujoco.py index 900ab37..6933696 100644 --- a/examples/show_in_mujoco.py +++ b/examples/show_in_mujoco.py @@ -7,8 +7,9 @@ """ Show a robot description, specified from the command line, using MuJoCo. -This example requires MuJoCo, which is installed by `pip install mujoco`, and -the MuJoCo viewer installed by `pip install mujoco-python-viewer`. +This example is equivalent to `python -m robot_descriptions show_in_mujoco`. It +requires MuJoCo, which is installed by `pip install mujoco`, and the MuJoCo +viewer installed by `pip install mujoco-python-viewer`. """ import argparse diff --git a/examples/show_in_pybullet.py b/examples/show_in_pybullet.py index 8575bac..76b2c95 100644 --- a/examples/show_in_pybullet.py +++ b/examples/show_in_pybullet.py @@ -7,7 +7,8 @@ """ Show a robot description, specified from the command line, using PyBullet. -This example requires PyBullet, which is installed by ``pip install pybullet``. +This example is equivalent to `python -m robot_descriptions show_in_pybullet`. +It requires PyBullet, which can be installed by `pip install pybullet`. """ import argparse diff --git a/examples/show_in_yourdfpy.py b/examples/show_in_yourdfpy.py index 0ed83cc..168efba 100644 --- a/examples/show_in_yourdfpy.py +++ b/examples/show_in_yourdfpy.py @@ -7,14 +7,9 @@ """ Show a robot description, specified from the command line, using yourdfpy. -Note: - See ``robot_descriptions/_command_line.py`` for a more advanced - implementation, including the ability to set the robot configuration or - show collision meshes. - -This example requires `yourdfpy` which is an optional dependency. It can be -installed separately (``pip install yourdfpy``), or when robot descriptions are -installed with optional dependencies ``pip install robot_descriptions[opts]``. +This example is equivalent to `python -m robot_descriptions show_in_yourdfpy`. +It requires `yourdfpy`, an optional dependency that can be installed by `pip +install yourdfpy`. """ import argparse diff --git a/pyproject.toml b/pyproject.toml index 6ef1bfc..b9dbf93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ opts = [ ] [project.scripts] -robot_descriptions = "robot_descriptions._command_line:main" +robot_descriptions = "robot_descriptions.__main__:main" [project.urls] Homepage = "https://github.com/robot-descriptions" diff --git a/robot_descriptions/_command_line.py b/robot_descriptions/__main__.py similarity index 52% rename from robot_descriptions/_command_line.py rename to robot_descriptions/__main__.py index b16aac7..c7fd11b 100644 --- a/robot_descriptions/_command_line.py +++ b/robot_descriptions/__main__.py @@ -48,23 +48,53 @@ def get_argument_parser() -> argparse.ArgumentParser: help="list all available robot descriptions", ) - # show -------------------------------------------------------------------- - parser_show = subparsers.add_parser( - "show", - help="load and display a given robot description", + # show_in_meshcat -------------------------------------------------------- + parser_meshcat = subparsers.add_parser( + "show_in_meshcat", + help="load and display a given robot description in Meshcat", ) - parser_show.add_argument( + parser_meshcat.add_argument( "name", help="name of the robot description", ) - parser_show.add_argument( + + # show_in_mujoco -------------------------------------------------------- + parser_mujoco = subparsers.add_parser( + "show_in_mujoco", + help="load and display a given robot description in mujoco", + ) + parser_mujoco.add_argument( + "name", + help="name of the robot description", + ) + + # show_in_pybullet -------------------------------------------------------- + parser_pybullet = subparsers.add_parser( + "show_in_pybullet", + help="load and display a given robot description in pybullet", + ) + parser_pybullet.add_argument( + "name", + help="name of the robot description", + ) + + # show_in_yourdfpy -------------------------------------------------------- + parser_yourdfpy = subparsers.add_parser( + "show_in_yourdfpy", + help="load and display a given robot description with yourdfpy", + ) + parser_yourdfpy.add_argument( + "name", + help="name of the robot description", + ) + parser_yourdfpy.add_argument( "-c", "--configuration", nargs="+", type=float, help="configuration of the visualized robot description", ) - parser_show.add_argument( + parser_yourdfpy.add_argument( "--collision", action="store_true", help="use collision geometry for the visualized robot description", @@ -96,7 +126,88 @@ def list_descriptions(): print(f"- {name} [{formats}]") -def show( +def show_in_meshcat(name: str) -> None: + """Show a robot description in MeshCat. + + Args: + name: Name of the robot description. + """ + try: + from pinocchio.visualize import MeshcatVisualizer + except ImportError as e: + raise ImportError( + "Pinocchio not found, try for instance `conda install pinocchio`" + ) from e + + from robot_descriptions.loaders.pinocchio import load_robot_description + + try: + robot = load_robot_description(name) + except ModuleNotFoundError: + robot = load_robot_description(f"{name}_description") + + robot.setVisualizer(MeshcatVisualizer()) + robot.initViewer(open=True) + robot.loadViewerModel() + robot.display(robot.q0) + + input("Press Enter to close MeshCat and terminate... ") + + +def show_in_mujoco(name: str) -> None: + """Show a robot description in MuJoCo. + + Args: + name: Name of the robot description. + """ + import mujoco + + try: + import mujoco_viewer + except ImportError as e: + raise ImportError( + "MuJoCo viewer not found, try `pip install mujoco-python-viewer`" + ) from e + + from robot_descriptions.loaders.mujoco import load_robot_description + + try: + model = load_robot_description(name) + except ModuleNotFoundError: + model = load_robot_description(f"{name}_mj_description") + + data = mujoco.MjData(model) + viewer = mujoco_viewer.MujocoViewer(model, data) + mujoco.mj_step(model, data) # step at least once to load model in viewer + while viewer.is_alive: + viewer.render() + viewer.close() + + +def show_in_pybullet(name: str) -> None: + """Show a robot description in PyBullet. + + Args: + name: Name of the robot description. + """ + import pybullet + + from robot_descriptions.loaders.pybullet import load_robot_description + + pybullet.connect(pybullet.GUI) + pybullet.configureDebugVisualizer(pybullet.COV_ENABLE_SHADOWS, 0) + pybullet.configureDebugVisualizer(pybullet.COV_ENABLE_GUI, 0) + + try: + load_robot_description(name) + except ModuleNotFoundError: + load_robot_description(f"{name}_description") + + input("Press Enter to close PyBullet and terminate... ") + pybullet.disconnect() + + +def show_in_yourdfpy( name: str, configuration: List[float], collision: bool, @@ -114,8 +225,8 @@ def show( module = import_module(f"robot_descriptions.{name}_description") if not hasattr(module, "URDF_PATH"): raise ValueError( - "show command only applies to URDF, check out the " - "`show_in_mujoco.py` example for MJCF descriptions" + "This command only works with URDF descriptions, use " + "`show_in_mujoco` for MJCF descriptions" ) try: @@ -156,7 +267,7 @@ def animate(name: str) -> None: if not hasattr(module, "URDF_PATH"): raise ValueError( "animation is only available for URDF descriptions, " - "check out the ``show_in_mujoco.py`` example for MJCF" + "check out the `show_in_mujoco.py` example for MJCF" ) print( @@ -172,9 +283,19 @@ def main(argv=None): args = parser.parse_args(argv) if args.subcmd == "list": list_descriptions() - elif args.subcmd == "show": - show(args.name, args.configuration, args.collision) + elif args.subcmd == "show_in_meshcat": + show_in_meshcat(args.name) + elif args.subcmd == "show_in_mujoco": + show_in_mujoco(args.name) + elif args.subcmd == "show_in_pybullet": + show_in_pybullet(args.name) + elif args.subcmd == "show_in_yourdfpy": + show_in_yourdfpy(args.name, args.configuration, args.collision) elif args.subcmd == "animate": animate(args.name) else: # no subcommand parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/robot_descriptions/_cache.py b/robot_descriptions/_cache.py index 310c64d..7a396aa 100644 --- a/robot_descriptions/_cache.py +++ b/robot_descriptions/_cache.py @@ -43,7 +43,7 @@ def update( op_code: Integer that can be compared against Operation IDs and stage IDs. cur_count: Current item count. - max_count: Maximum item count, or ``None`` if there is none. + max_count: Maximum item count, or `None` if there is none. message: Unused here. """ self.progress.total = max_count @@ -112,8 +112,8 @@ def clone_to_cache(description_name: str, commit: Optional[str] = None) -> str: Notes: By default, robot descriptions are cached to - ``~/.cache/robot_descriptions``. This behavior can be overriden by - setting the ``ROBOT_DESCRIPTIONS_CACHE`` environment variable to an + `~/.cache/robot_descriptions`. This behavior can be overriden by + setting the `ROBOT_DESCRIPTIONS_CACHE` environment variable to an alternative path. """ try: