From 7ffda13abc658c65638f700cb38cbe2568c47db2 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 12 Feb 2024 13:20:55 -0500 Subject: [PATCH 1/3] Introduce Separate Builder (+ Moved Material files) --- obj2mjcf/MJCFBuilder.py | 253 ++++++++++++++++++++++++++++++++++++++++ obj2mjcf/Material.py | 72 ++++++++++++ obj2mjcf/_cli.py | 202 ++------------------------------ 3 files changed, 332 insertions(+), 195 deletions(-) create mode 100644 obj2mjcf/MJCFBuilder.py create mode 100644 obj2mjcf/Material.py diff --git a/obj2mjcf/MJCFBuilder.py b/obj2mjcf/MJCFBuilder.py new file mode 100644 index 0000000..a130a30 --- /dev/null +++ b/obj2mjcf/MJCFBuilder.py @@ -0,0 +1,253 @@ +""" +MJCFBuilder.py +Description: + This file contains the class that is used to build MJCF files. +""" +import logging +import os +from lxml import etree +from pathlib import Path + +from typing import List, Tuple, Union, Any + +import mujoco +import trimesh +from termcolor import cprint + +from obj2mjcf.Material import Material + + +# 2-space indentation for the generated XML. +_XML_INDENTATION = " " + + +class MJCFBuilder: + def __init__( + self, + filename: Path, + mesh: Union[trimesh.base.Trimesh,Any], + materials: List[Material], + work_dir: Path = None, + ): + self.filename = filename + self.mesh = mesh + self.materials = materials + + self.work_dir = work_dir + if self.work_dir is None: + self.work_dir = filename.parent / filename.stem + + # Define variables that will be defined later + self.tree = None + + def add_visual_and_collision_default_classes( + self, + root: etree.Element, + ): + # Define the default element + default_elem = etree.SubElement(root, "default") + + # Define visual defaults + visual_default_elem = etree.SubElement(default_elem, "default") + visual_default_elem.attrib["class"] = "visual" + etree.SubElement( + visual_default_elem, + "geom", + group="2", + type="mesh", + contype="0", + conaffinity="0", + ) + + # Define collision defaults + collision_default_elem = etree.SubElement(default_elem, "default") + collision_default_elem.attrib["class"] = "collision" + etree.SubElement(collision_default_elem, "geom", group="3", type="mesh") + + def add_assets(self, root: etree.Element, mtls: List[Material]) -> etree.Element: + # Define the assets element + asset_elem = etree.SubElement(root, "asset") + + for material in mtls: + if material.map_Kd is not None: + # Create the texture asset. + texture = Path(material.map_Kd) + etree.SubElement( + asset_elem, + "texture", + type="2d", + name=texture.stem, + file=texture.name, + ) + # Reference the texture asset in a material asset. + etree.SubElement( + asset_elem, + "material", + name=material.name, + texture=texture.stem, + specular=material.mjcf_specular(), + shininess=material.mjcf_shininess(), + ) + else: + etree.SubElement( + asset_elem, + "material", + name=material.name, + specular=material.mjcf_specular(), + shininess=material.mjcf_shininess(), + rgba=material.mjcf_rgba(), + ) + + return asset_elem + + def add_visual_geometries( + self, + obj_body: etree.Element, + asset_elem: etree.Element, + ): + # Constants + filename = self.filename + mesh = self.mesh + materials = self.materials + + process_mtl = len(materials) > 0 + + # Add visual geometries to object body + if isinstance(mesh, trimesh.base.Trimesh): + meshname = Path(f"{filename.stem}.obj") + # Add the mesh to assets. + etree.SubElement(asset_elem, "mesh", file=str(meshname)) + # Add the geom to the worldbody. + if process_mtl: + e_ = etree.SubElement( + obj_body, "geom", material=materials[0].name, mesh=str(meshname.stem) + ) + e_.attrib["class"] = "visual" + else: + e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) + e_.attrib["class"] = "visual" + else: + for i, (name, geom) in enumerate(mesh.geometry.items()): + meshname = Path(f"{filename.stem}_{i}.obj") + # Add the mesh to assets. + etree.SubElement(asset_elem, "mesh", file=str(meshname)) + # Add the geom to the worldbody. + if process_mtl: + e_ = etree.SubElement( + obj_body, "geom", mesh=meshname.stem, material=name + ) + e_.attrib["class"] = "visual" + else: + e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) + e_.attrib["class"] = "visual" + + def add_collision_geometries( + self, + obj_body: etree.Element, + asset_elem: etree.Element, + decomp_success: bool = False, + ): + # Constants + filename = self.filename + mesh = self.mesh + + work_dir = self.work_dir + + if decomp_success: + # Find collision files from the decomposed convex hulls. + collisions = [ + x for x in work_dir.glob("**/*") if x.is_file() and "collision" in x.name + ] + collisions.sort(key=lambda x: int(x.stem.split("_")[-1])) + + for collision in collisions: + etree.SubElement(asset_elem, "mesh", file=collision.name) + e_ = etree.SubElement(obj_body, "geom", mesh=collision.stem) + e_.attrib["class"] = "collision" + else: + # If no decomposed convex hulls were created, use the original mesh as the + # collision mesh. + if isinstance(mesh, trimesh.base.Trimesh): + meshname = Path(f"{filename.stem}.obj") + e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) + e_.attrib["class"] = "collision" + else: + for i, (name, geom) in enumerate(mesh.geometry.items()): + meshname = Path(f"{filename.stem}_{i}.obj") + e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) + e_.attrib["class"] = "collision" + + def build( + self, + add_free_joint: bool = False, + ): + # Constants + filename = self.filename + mesh = self.mesh + mtls = self.materials + + # Start assembling xml tree + root = etree.Element("mujoco", model=filename.stem) + + # Add Defaults + Assets + self.add_visual_and_collision_default_classes(root) + asset_elem = self.add_assets(root, mtls) + + # Add Worldbody + worldbody_elem = etree.SubElement(root, "worldbody") + obj_body = etree.SubElement(worldbody_elem, "body", name=filename.stem) + if add_free_joint: + etree.SubElement(obj_body, "freejoint") + + # Add visual and collision geometries to object body + self.add_visual_geometries(obj_body, asset_elem) + self.add_collision_geometries(obj_body, asset_elem) + + # Collect Tree + tree = etree.ElementTree(root) + etree.indent(tree, space=_XML_INDENTATION, level=0) + + self.tree = tree + + def compile_model(self): + # Constants + filename = self.filename + work_dir = self.work_dir + + # Pull up tree if possible + tree = self.tree + if tree is None: + raise ValueError("Tree has not been defined yet.") + + # Create the work directory if it does not exist. + try: + tmp_path = work_dir / "tmp.xml" + tree.write(tmp_path, encoding="utf-8") + model = mujoco.MjModel.from_xml_path(str(tmp_path)) + data = mujoco.MjData(model) + mujoco.mj_step(model, data) + cprint(f"{filename} compiled successfully!", "green") + except Exception as e: + cprint(f"Error compiling model: {e}", "red") + finally: + if tmp_path.exists(): + tmp_path.unlink() + + def save_mjcf( + self, + ): + # Constants + filename = self.filename + work_dir = self.work_dir + + # Input Processing + + # Pull up tree if possible + tree = self.tree + if tree is None: + raise ValueError("Tree has not been defined yet.") + + # Save the MJCF file. + xml_path = str(work_dir / f"{filename.stem}.xml") + tree.write(xml_path, encoding="utf-8") + logging.info(f"Saved MJCF to {xml_path}") \ No newline at end of file diff --git a/obj2mjcf/Material.py b/obj2mjcf/Material.py new file mode 100644 index 0000000..25a51ed --- /dev/null +++ b/obj2mjcf/Material.py @@ -0,0 +1,72 @@ +from typing import Optional, Sequence +from dataclasses import dataclass, field + + +# MTL fields relevant to MuJoCo. +_MTL_FIELDS = ( + # Ambient, diffuse and specular colors. + "Ka", + "Kd", + "Ks", + # d or Tr are used for the rgba transparency. + "d", + "Tr", + # Shininess. + "Ns", + # References a texture file. + "map_Kd", +) + +# Character used to denote a comment in an MTL file. +_MTL_COMMENT_CHAR = "#" + +@dataclass +class Material: + name: str + Ka: Optional[str] = None + Kd: Optional[str] = None + Ks: Optional[str] = None + d: Optional[str] = None + Tr: Optional[str] = None + Ns: Optional[str] = None + map_Kd: Optional[str] = None + + @staticmethod + def from_string(lines: Sequence[str]) -> "Material": + """Construct a Material object from a string.""" + attrs = {"name": lines[0].split(" ")[1].strip()} + for line in lines[1:]: + for attr in _MTL_FIELDS: + if line.startswith(attr): + elems = line.split(" ")[1:] + elems = [elem for elem in elems if elem != ""] + attrs[attr] = " ".join(elems) + break + return Material(**attrs) + + def mjcf_rgba(self) -> str: + Kd = self.Kd or "1.0 1.0 1.0" + if self.d is not None: # alpha + alpha = self.d + elif self.Tr is not None: # 1 - alpha + alpha = str(1.0 - float(self.Tr)) + else: + alpha = "1.0" + # TODO(kevin): Figure out how to use Ka for computing rgba. + return f"{Kd} {alpha}" + + def mjcf_shininess(self) -> str: + if self.Ns is not None: + # Normalize Ns value to [0, 1]. Ns values normally range from 0 to 1000. + Ns = float(self.Ns) / 1_000 + else: + Ns = 0.5 + return f"{Ns}" + + def mjcf_specular(self) -> str: + if self.Ks is not None: + # Take the average of the specular RGB values. + Ks = sum(list(map(float, self.Ks.split(" ")))) / 3 + else: + Ks = 0.5 + return f"{Ks}" \ No newline at end of file diff --git a/obj2mjcf/_cli.py b/obj2mjcf/_cli.py index 69034d1..0839305 100644 --- a/obj2mjcf/_cli.py +++ b/obj2mjcf/_cli.py @@ -18,6 +18,9 @@ from PIL import Image from termcolor import cprint +from obj2mjcf.MJCFBuilder import MJCFBuilder +from obj2mjcf.Material import Material + # Find the V-HACD v4.0 executable in the system path. # Note trimesh has not updated their code to work with v4.0 which is why we do not use # their `convex_decomposition` function. @@ -30,24 +33,6 @@ # 2-space indentation for the generated XML. _XML_INDENTATION = " " -# MTL fields relevant to MuJoCo. -_MTL_FIELDS = ( - # Ambient, diffuse and specular colors. - "Ka", - "Kd", - "Ks", - # d or Tr are used for the rgba transparency. - "d", - "Tr", - # Shininess. - "Ns", - # References a texture file. - "map_Kd", -) - -# Character used to denote a comment in an MTL file. -_MTL_COMMENT_CHAR = "#" - class FillMode(enum.Enum): FLOOD = enum.auto() @@ -106,58 +91,6 @@ class Args: """add a free joint to the root body""" -@dataclass -class Material: - name: str - Ka: Optional[str] = None - Kd: Optional[str] = None - Ks: Optional[str] = None - d: Optional[str] = None - Tr: Optional[str] = None - Ns: Optional[str] = None - map_Kd: Optional[str] = None - - @staticmethod - def from_string(lines: Sequence[str]) -> "Material": - """Construct a Material object from a string.""" - attrs = {"name": lines[0].split(" ")[1].strip()} - for line in lines[1:]: - for attr in _MTL_FIELDS: - if line.startswith(attr): - elems = line.split(" ")[1:] - elems = [elem for elem in elems if elem != ""] - attrs[attr] = " ".join(elems) - break - return Material(**attrs) - - def mjcf_rgba(self) -> str: - Kd = self.Kd or "1.0 1.0 1.0" - if self.d is not None: # alpha - alpha = self.d - elif self.Tr is not None: # 1 - alpha - alpha = str(1.0 - float(self.Tr)) - else: - alpha = "1.0" - # TODO(kevin): Figure out how to use Ka for computing rgba. - return f"{Kd} {alpha}" - - def mjcf_shininess(self) -> str: - if self.Ns is not None: - # Normalize Ns value to [0, 1]. Ns values normally range from 0 to 1000. - Ns = float(self.Ns) / 1_000 - else: - Ns = 0.5 - return f"{Ns}" - - def mjcf_specular(self) -> str: - if self.Ks is not None: - # Take the average of the specular RGB values. - Ks = sum(list(map(float, self.Ks.split(" ")))) / 3 - else: - Ks = 0.5 - return f"{Ks}" - - def resize_texture(filename: Path, resize_percent) -> None: """Resize a texture to a percentage of its original size.""" if resize_percent == 1.0: @@ -407,137 +340,16 @@ def process_obj(filename: Path, args: Args) -> None: f.write("".join(lines)) # Build an MJCF. - root = etree.Element("mujoco", model=filename.stem) - - # Add visual and collision default classes. - default_elem = etree.SubElement(root, "default") - visual_default_elem = etree.SubElement(default_elem, "default") - visual_default_elem.attrib["class"] = "visual" - etree.SubElement( - visual_default_elem, - "geom", - group="2", - type="mesh", - contype="0", - conaffinity="0", - ) - collision_default_elem = etree.SubElement(default_elem, "default") - collision_default_elem.attrib["class"] = "collision" - etree.SubElement(collision_default_elem, "geom", group="3", type="mesh") - - # Add assets. - asset_elem = etree.SubElement(root, "asset") - for material in mtls: - if material.map_Kd is not None: - # Create the texture asset. - texture = Path(material.map_Kd) - etree.SubElement( - asset_elem, - "texture", - type="2d", - name=texture.stem, - file=texture.name, - ) - # Reference the texture asset in a material asset. - etree.SubElement( - asset_elem, - "material", - name=material.name, - texture=texture.stem, - specular=material.mjcf_specular(), - shininess=material.mjcf_shininess(), - ) - else: - etree.SubElement( - asset_elem, - "material", - name=material.name, - specular=material.mjcf_specular(), - shininess=material.mjcf_shininess(), - rgba=material.mjcf_rgba(), - ) - - worldbody_elem = etree.SubElement(root, "worldbody") - obj_body = etree.SubElement(worldbody_elem, "body", name=filename.stem) - if args.add_free_joint: - etree.SubElement(obj_body, "freejoint") - - # Add visual geoms. - if isinstance(mesh, trimesh.base.Trimesh): - meshname = Path(f"{filename.stem}.obj") - # Add the mesh to assets. - etree.SubElement(asset_elem, "mesh", file=str(meshname)) - # Add the geom to the worldbody. - if process_mtl: - e_ = etree.SubElement( - obj_body, "geom", material=material.name, mesh=str(meshname.stem) - ) - e_.attrib["class"] = "visual" - else: - e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) - e_.attrib["class"] = "visual" - else: - for i, (name, geom) in enumerate(mesh.geometry.items()): - meshname = Path(f"{filename.stem}_{i}.obj") - # Add the mesh to assets. - etree.SubElement(asset_elem, "mesh", file=str(meshname)) - # Add the geom to the worldbody. - if process_mtl: - e_ = etree.SubElement( - obj_body, "geom", mesh=meshname.stem, material=name - ) - e_.attrib["class"] = "visual" - else: - e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) - e_.attrib["class"] = "visual" - - # Add collision geoms. - if decomp_success: - # Find collision files from the decomposed convex hulls. - collisions = [ - x for x in work_dir.glob("**/*") if x.is_file() and "collision" in x.name - ] - collisions.sort(key=lambda x: int(x.stem.split("_")[-1])) - - for collision in collisions: - etree.SubElement(asset_elem, "mesh", file=collision.name) - e_ = etree.SubElement(obj_body, "geom", mesh=collision.stem) - e_.attrib["class"] = "collision" - else: - # If no decomposed convex hulls were created, use the original mesh as the - # collision mesh. - if isinstance(mesh, trimesh.base.Trimesh): - e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) - e_.attrib["class"] = "collision" - else: - for i, (name, geom) in enumerate(mesh.geometry.items()): - meshname = Path(f"{filename.stem}_{i}.obj") - e_ = etree.SubElement(obj_body, "geom", mesh=meshname.stem) - e_.attrib["class"] = "collision" - - tree = etree.ElementTree(root) - etree.indent(tree, space=_XML_INDENTATION, level=0) + builder = MJCFBuilder(filename, mesh, mtls) + tree = builder.build() # Compile and step the physics to check for any errors. if args.compile_model: - try: - tmp_path = work_dir / "tmp.xml" - tree.write(tmp_path, encoding="utf-8") - model = mujoco.MjModel.from_xml_path(str(tmp_path)) - data = mujoco.MjData(model) - mujoco.mj_step(model, data) - cprint(f"{filename} compiled successfully!", "green") - except Exception as e: - cprint(f"Error compiling model: {e}", "red") - finally: - if tmp_path.exists(): - tmp_path.unlink() + builder.compile_model() # Dump. if args.save_mjcf: - xml_path = str(work_dir / f"{filename.stem}.xml") - tree.write(xml_path, encoding="utf-8") - logging.info(f"Saved MJCF to {xml_path}") + builder.save_mjcf() def main() -> None: From b39b59cd9b914a7d962d88d1ad4093b57d7aa3ba Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 12 Feb 2024 15:46:19 -0500 Subject: [PATCH 2/3] Update after fixing all "make format" issues --- .flake8 | 11 ++-- obj2mjcf/_cli.py | 10 ++-- obj2mjcf/{Material.py => material.py} | 8 ++- obj2mjcf/{MJCFBuilder.py => mjcf_builder.py} | 58 +++++++++++--------- 4 files changed, 47 insertions(+), 40 deletions(-) rename obj2mjcf/{Material.py => material.py} (94%) rename obj2mjcf/{MJCFBuilder.py => mjcf_builder.py} (88%) diff --git a/.flake8 b/.flake8 index 5174d49..5e52f64 100644 --- a/.flake8 +++ b/.flake8 @@ -1,10 +1,13 @@ [flake8] -exclude = .git +exclude = .git, ./venv-obj2mjcf max-line-length = 80 ignore = - E203, # whitespace before colon (black default) - E501, # line too long ( characters) - W503, # line break before binary operator + # whitespace before colon (black default) + E203, + # line too long ( characters) + E501, + # line break before binary operator + W503, per-file-ignores = */__init__.py: F401 diff --git a/obj2mjcf/_cli.py b/obj2mjcf/_cli.py index 0839305..ad4748b 100644 --- a/obj2mjcf/_cli.py +++ b/obj2mjcf/_cli.py @@ -9,17 +9,15 @@ import tempfile from dataclasses import dataclass, field from pathlib import Path -from typing import List, Optional, Sequence +from typing import List, Optional -import mujoco import trimesh import tyro -from lxml import etree from PIL import Image from termcolor import cprint +from obj2mjcf.Material import _MTL_COMMENT_CHAR, Material from obj2mjcf.MJCFBuilder import MJCFBuilder -from obj2mjcf.Material import Material # Find the V-HACD v4.0 executable in the system path. # Note trimesh has not updated their code to work with v4.0 which is why we do not use @@ -340,8 +338,8 @@ def process_obj(filename: Path, args: Args) -> None: f.write("".join(lines)) # Build an MJCF. - builder = MJCFBuilder(filename, mesh, mtls) - tree = builder.build() + builder = MJCFBuilder(filename, mesh, mtls, decomp_success=decomp_success) + builder.build() # Compile and step the physics to check for any errors. if args.compile_model: diff --git a/obj2mjcf/Material.py b/obj2mjcf/material.py similarity index 94% rename from obj2mjcf/Material.py rename to obj2mjcf/material.py index 25a51ed..a1c83f4 100644 --- a/obj2mjcf/Material.py +++ b/obj2mjcf/material.py @@ -1,6 +1,7 @@ -from typing import Optional, Sequence -from dataclasses import dataclass, field +"""A class for handling MuJoCo material properties.""" +from dataclasses import dataclass +from typing import Optional, Sequence # MTL fields relevant to MuJoCo. _MTL_FIELDS = ( @@ -20,6 +21,7 @@ # Character used to denote a comment in an MTL file. _MTL_COMMENT_CHAR = "#" + @dataclass class Material: name: str @@ -69,4 +71,4 @@ def mjcf_specular(self) -> str: Ks = sum(list(map(float, self.Ks.split(" ")))) / 3 else: Ks = 0.5 - return f"{Ks}" \ No newline at end of file + return f"{Ks}" diff --git a/obj2mjcf/MJCFBuilder.py b/obj2mjcf/mjcf_builder.py similarity index 88% rename from obj2mjcf/MJCFBuilder.py rename to obj2mjcf/mjcf_builder.py index a130a30..79e1f1e 100644 --- a/obj2mjcf/MJCFBuilder.py +++ b/obj2mjcf/mjcf_builder.py @@ -3,46 +3,46 @@ Description: This file contains the class that is used to build MJCF files. """ + import logging -import os -from lxml import etree from pathlib import Path - -from typing import List, Tuple, Union, Any +from typing import Any, List, Union import mujoco import trimesh +from lxml import etree from termcolor import cprint from obj2mjcf.Material import Material - # 2-space indentation for the generated XML. _XML_INDENTATION = " " class MJCFBuilder: def __init__( - self, - filename: Path, - mesh: Union[trimesh.base.Trimesh,Any], - materials: List[Material], - work_dir: Path = None, + self, + filename: Path, + mesh: Union[trimesh.base.Trimesh, Any], + materials: List[Material], + work_dir: Path = Path(), + decomp_success: bool = False, ): self.filename = filename self.mesh = mesh self.materials = materials + self.decomp_success = decomp_success self.work_dir = work_dir - if self.work_dir is None: + if self.work_dir == Path(): self.work_dir = filename.parent / filename.stem # Define variables that will be defined later self.tree = None def add_visual_and_collision_default_classes( - self, - root: etree.Element, + self, + root: etree.Element, ): # Define the default element default_elem = etree.SubElement(root, "default") @@ -101,9 +101,9 @@ def add_assets(self, root: etree.Element, mtls: List[Material]) -> etree.Element return asset_elem def add_visual_geometries( - self, - obj_body: etree.Element, - asset_elem: etree.Element, + self, + obj_body: etree.Element, + asset_elem: etree.Element, ): # Constants filename = self.filename @@ -120,7 +120,10 @@ def add_visual_geometries( # Add the geom to the worldbody. if process_mtl: e_ = etree.SubElement( - obj_body, "geom", material=materials[0].name, mesh=str(meshname.stem) + obj_body, + "geom", + material=materials[0].name, + mesh=str(meshname.stem), ) e_.attrib["class"] = "visual" else: @@ -142,21 +145,23 @@ def add_visual_geometries( e_.attrib["class"] = "visual" def add_collision_geometries( - self, - obj_body: etree.Element, - asset_elem: etree.Element, - decomp_success: bool = False, + self, + obj_body: etree.Element, + asset_elem: etree.Element, ): # Constants filename = self.filename mesh = self.mesh + decomp_success = self.decomp_success work_dir = self.work_dir if decomp_success: # Find collision files from the decomposed convex hulls. collisions = [ - x for x in work_dir.glob("**/*") if x.is_file() and "collision" in x.name + x + for x in work_dir.glob("**/*") + if x.is_file() and "collision" in x.name ] collisions.sort(key=lambda x: int(x.stem.split("_")[-1])) @@ -178,12 +183,11 @@ def add_collision_geometries( e_.attrib["class"] = "collision" def build( - self, - add_free_joint: bool = False, - ): + self, + add_free_joint: bool = False, + ) -> None: # Constants filename = self.filename - mesh = self.mesh mtls = self.materials # Start assembling xml tree @@ -250,4 +254,4 @@ def save_mjcf( # Save the MJCF file. xml_path = str(work_dir / f"{filename.stem}.xml") tree.write(xml_path, encoding="utf-8") - logging.info(f"Saved MJCF to {xml_path}") \ No newline at end of file + logging.info(f"Saved MJCF to {xml_path}") From e036bede85a264b59dcce391ef5d8b28ec3e13c4 Mon Sep 17 00:00:00 2001 From: Kwesi Rutledge Date: Mon, 12 Feb 2024 16:01:05 -0500 Subject: [PATCH 3/3] Changed the case of Material and MJCFBuilder in imports --- obj2mjcf/_cli.py | 4 ++-- obj2mjcf/mjcf_builder.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/obj2mjcf/_cli.py b/obj2mjcf/_cli.py index ad4748b..39f83aa 100644 --- a/obj2mjcf/_cli.py +++ b/obj2mjcf/_cli.py @@ -16,8 +16,8 @@ from PIL import Image from termcolor import cprint -from obj2mjcf.Material import _MTL_COMMENT_CHAR, Material -from obj2mjcf.MJCFBuilder import MJCFBuilder +from obj2mjcf.material import _MTL_COMMENT_CHAR, Material +from obj2mjcf.mjcf_builder import MJCFBuilder # Find the V-HACD v4.0 executable in the system path. # Note trimesh has not updated their code to work with v4.0 which is why we do not use diff --git a/obj2mjcf/mjcf_builder.py b/obj2mjcf/mjcf_builder.py index 79e1f1e..1c1070e 100644 --- a/obj2mjcf/mjcf_builder.py +++ b/obj2mjcf/mjcf_builder.py @@ -13,7 +13,7 @@ from lxml import etree from termcolor import cprint -from obj2mjcf.Material import Material +from obj2mjcf.material import Material # 2-space indentation for the generated XML. _XML_INDENTATION = " "