Skip to content

Commit

Permalink
Added MJCF methods to all robot components.
Browse files Browse the repository at this point in the history
  • Loading branch information
senthurayyappan committed Dec 17, 2024
1 parent 86e78af commit 951473a
Show file tree
Hide file tree
Showing 8 changed files with 801 additions and 119 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

onshape_api/**/*.urdf
onshape_api/**/*.stl
onshape_api/**/*.png
onshape_api/**/*.prof
onshape_api/**/*.json
onshape_api/**/*.xml

examples/**/*.urdf
examples/**/*.stl
examples/**/*.png
Expand Down
48 changes: 39 additions & 9 deletions onshape_api/connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -1008,15 +1008,16 @@ class Asset:

def __init__(
self,
did: str,
wtype: str,
wid: str,
eid: str,
client: Client,
transform: np.ndarray,
file_name: str,
did: str = "",
wtype: str = "",
wid: str = "",
eid: str = "",
client: Optional[Client] = None,
transform: Optional[np.ndarray] = None,
is_rigid_assembly: bool = False,
partID: Optional[str] = None,
is_from_file: bool = False,
) -> None:
"""
Initialize the Asset object.
Expand All @@ -1041,12 +1042,18 @@ def __init__(
self.file_name = file_name
self.is_rigid_assembly = is_rigid_assembly
self.partID = partID
self.is_from_file = is_from_file

self._file_path = None

@property
def absolute_path(self) -> str:
"""
Returns the file path of the mesh file.
"""
if self.is_from_file:
return self._file_path

# if meshes directory does not exist, create it
if not os.path.exists(os.path.join(CURRENT_DIR, MESHES_DIR)):
os.makedirs(os.path.join(CURRENT_DIR, MESHES_DIR))
Expand Down Expand Up @@ -1097,7 +1104,7 @@ async def download(self) -> None:
except Exception as e:
LOGGER.error(f"Failed to download {self.file_name}: {e}")

def to_xml(self, root: Optional[ET.Element] = None) -> str:
def to_mjcf(self, root: ET.Element) -> None:
"""
Returns the XML representation of the asset, which is a mesh file.
Expand All @@ -1112,10 +1119,33 @@ def to_xml(self, root: Optional[ET.Element] = None) -> str:
... file_name="mesh.stl",
... is_rigid_assembly=True
... )
>>> asset.to_xml()
>>> asset.to_mjcf()
<mesh name="Part-1-1" file="Part-1-1.stl" />
"""
asset = ET.Element("mesh") if root is None else ET.SubElement(root, "mesh")
asset.set("mesh", self.file_name)
asset.set("name", self.file_name)
asset.set("file", self.relative_path)

@classmethod
def from_file(cls, file_path: str) -> "Asset":
"""
Create an Asset object from a mesh file.
Args:
file_path: Path to the mesh file.
client: Onshape API client object.
Returns:
Asset: Asset object representing the mesh file.
Examples:
>>> asset = Asset.from_file("mesh.stl", client)
"""
file_name = os.path.basename(file_path)
asset = cls(
file_name=file_name.split(".")[0],
is_from_file=True,
)

asset._file_path = file_path
return asset
2 changes: 1 addition & 1 deletion onshape_api/models/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

from pydantic import BaseModel, Field, field_validator

__all__ = ["ElementType", "Element"]
__all__ = ["Element", "ElementType"]


class ElementType(str, Enum):
Expand Down
129 changes: 129 additions & 0 deletions onshape_api/models/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,40 @@
- **MeshGeometry**: Represents a mesh geometry.
"""

import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Optional

import lxml.etree as ET

from onshape_api.utilities import format_number, xml_escape


class GeometryType(str, Enum):
"""
Enumerates the possible geometry types in Onshape.
Attributes:
BOX (str): Box geometry.
CYLINDER (str): Cylinder geometry.
SPHERE (str): Sphere geometry.
MESH (str): Mesh geometry.
Examples:
>>> GeometryType.BOX
'BOX'
>>> GeometryType.CYLINDER
'CYLINDER'
"""

BOX = "box"
CYLINDER = "cylinder"
SPHERE = "sphere"
MESH = "mesh"


@dataclass
class BaseGeometry(ABC):
"""
Expand All @@ -30,10 +55,17 @@ class BaseGeometry(ABC):
@abstractmethod
def to_xml(self, root: Optional[ET.Element] = None) -> ET.Element: ...

@abstractmethod
def to_mjcf(self, root: ET.Element) -> None: ...

@classmethod
@abstractmethod
def from_xml(cls, element: ET.Element) -> "BaseGeometry": ...

@property
@abstractmethod
def geometry_type(self) -> str: ...


@dataclass
class BoxGeometry(BaseGeometry):
Expand Down Expand Up @@ -73,6 +105,25 @@ def to_xml(self, root: Optional[ET.Element] = None) -> ET.Element:
ET.SubElement(geometry, "box", size=" ".join(format_number(v) for v in self.size))
return geometry

def to_mjcf(self, root: ET.Element) -> None:
"""
Convert the box geometry to an MJCF element.
Args:
root: The root element to append the box geometry to.
Returns:
The MJCF element representing the box geometry.
Examples:
>>> box = BoxGeometry(size=(1.0, 2.0, 3.0))
>>> box.to_mjcf()
<Element 'geom' at 0x7f8b3c0b4c70>
"""
geom = root if root.tag == "geom" else ET.SubElement(root, "geom")
geom.set("type", GeometryType.BOX)
geom.set("size", " ".join(format_number(v) for v in self.size))

@classmethod
def from_xml(cls, element) -> "BoxGeometry":
"""
Expand All @@ -93,6 +144,10 @@ def from_xml(cls, element) -> "BoxGeometry":
size = tuple(float(v) for v in element.find("box").attrib["size"].split())
return cls(size)

@property
def geometry_type(self) -> str:
return GeometryType.BOX


@dataclass
class CylinderGeometry(BaseGeometry):
Expand Down Expand Up @@ -139,6 +194,25 @@ def to_xml(self, root: Optional[ET.Element] = None) -> ET.Element:
)
return geometry

def to_mjcf(self, root: ET.Element) -> None:
"""
Convert the cylinder geometry to an MJCF element.
Args:
root: The root element to append the cylinder geometry to.
Returns:
The MJCF element representing the cylinder geometry.
Examples:
>>> cylinder = CylinderGeometry(radius=1.0, length=2.0)
>>> cylinder.to_mjcf()
<Element 'geom' at 0x7f8b3c0b4c70>
"""
geom = root if root is not None and root.tag == "geom" else ET.SubElement(root, "geom")
geom.set("type", GeometryType.CYLINDER)
geom.set("size", f"{format_number(self.radius)} {format_number(self.length)}")

@classmethod
def from_xml(cls, element) -> "CylinderGeometry":
"""
Expand All @@ -160,6 +234,10 @@ def from_xml(cls, element) -> "CylinderGeometry":
length = float(element.find("cylinder").attrib["length"])
return cls(radius, length)

@property
def geometry_type(self) -> str:
return GeometryType.CYLINDER


@dataclass
class SphereGeometry(BaseGeometry):
Expand Down Expand Up @@ -199,6 +277,25 @@ def to_xml(self, root: Optional[ET.Element] = None) -> ET.Element:
ET.SubElement(geometry, "sphere", radius=format_number(self.radius))
return geometry

def to_mjcf(self, root: ET.Element) -> None:
"""
Convert the sphere geometry to an MJCF element.
Args:
root: The root element to append the sphere geometry to.
Returns:
The MJCF element representing the sphere geometry.
Examples:
>>> sphere = SphereGeometry(radius=1.0)
>>> sphere.to_mjcf()
<Element 'geom' at 0x7f8b3c0b4c70>
"""
geom = root if root is not None and root.tag == "geom" else ET.SubElement(root, "geom")
geom.set("type", GeometryType.SPHERE)
geom.set("size", format_number(self.radius))

@classmethod
def from_xml(cls, element) -> "SphereGeometry":
"""
Expand All @@ -219,6 +316,10 @@ def from_xml(cls, element) -> "SphereGeometry":
radius = float(element.find("sphere").attrib["radius"])
return cls(radius)

@property
def geometry_type(self) -> str:
return GeometryType.SPHERE


@dataclass
class MeshGeometry(BaseGeometry):
Expand Down Expand Up @@ -258,6 +359,25 @@ def to_xml(self, root: Optional[ET.Element] = None) -> ET.Element:
ET.SubElement(geometry, "mesh", filename=self.filename)
return geometry

def to_mjcf(self, root: ET.Element) -> None:
"""
Convert the mesh geometry to an MJCF element.
Args:
root: The root element to append the mesh geometry to.
Returns:
The MJCF element representing the mesh geometry.
Examples:
>>> mesh = MeshGeometry(filename="mesh.stl")
>>> mesh.to_mjcf()
<Element 'geom' at 0x7f8b3c0b4c70>
"""
geom = root if root is not None and root.tag == "geom" else ET.SubElement(root, "geom")
geom.set("type", GeometryType.MESH)
geom.set("mesh", self.mesh_name)

@classmethod
def from_xml(cls, element) -> "MeshGeometry":
"""
Expand All @@ -280,3 +400,12 @@ def from_xml(cls, element) -> "MeshGeometry":

def __post_init__(self) -> None:
self.filename = xml_escape(self.filename)

@property
def geometry_type(self) -> str:
return GeometryType.MESH

@property
def mesh_name(self) -> str:
file_name_w_ext = os.path.basename(self.filename)
return os.path.splitext(file_name_w_ext)[0]
Loading

0 comments on commit 951473a

Please sign in to comment.