Some checks failed
nightly / Test against latest dependencies (py3.10) (push) Has been cancelled
nightly / Test against latest dependencies (py3.13) (push) Has been cancelled
tests / tests (3.13, locked) (push) Has been cancelled
tests / tests (3.13, unlocked) (push) Has been cancelled
tests / pyright (3.10) (push) Has been cancelled
tests / lint-format (push) Has been cancelled
tests / tests (3.10, locked) (push) Has been cancelled
tests / tests (3.11, locked) (push) Has been cancelled
tests / tests (3.12, locked) (push) Has been cancelled
tests / pyright (3.11) (push) Has been cancelled
tests / pyright (3.12) (push) Has been cancelled
tests / pyright (3.13) (push) Has been cancelled
tests / ty-check (3.10) (push) Has been cancelled
tests / ty-check (3.11) (push) Has been cancelled
tests / ty-check (3.12) (push) Has been cancelled
tests / ty-check (3.13) (push) Has been cancelled
tests / stubs (push) Has been cancelled
tests / smoke-test (push) Has been cancelled
Docker / check_paths (push) Has been cancelled
docs / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Upstream: https://github.com/michaelgillett/mjlab Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047 Upstream-Branch: main
574 lines
19 KiB
Python
574 lines
19 KiB
Python
"""Test viser conversion functions with various robot models."""
|
|
|
|
from pathlib import Path
|
|
|
|
import mujoco
|
|
|
|
from mjlab.viewer.viser import (
|
|
create_site_mesh,
|
|
get_geom_texture_id,
|
|
group_geoms_by_visual_compat,
|
|
merge_geoms,
|
|
merge_sites,
|
|
mujoco_mesh_to_trimesh,
|
|
)
|
|
|
|
|
|
def load_robot_model(robot_name: str) -> mujoco.MjModel:
|
|
"""Load a robot model from the asset zoo."""
|
|
base_path = Path(__file__).parent.parent / "src/mjlab/asset_zoo/robots"
|
|
|
|
# Map robot names to their XML files.
|
|
robot_paths = {
|
|
"unitree_g1": base_path / "unitree_g1/xmls/g1.xml",
|
|
"unitree_go1": base_path / "unitree_go1/xmls/go1.xml",
|
|
}
|
|
|
|
if robot_name not in robot_paths:
|
|
raise ValueError(f"Unknown robot: {robot_name}")
|
|
|
|
xml_path = robot_paths[robot_name]
|
|
if not xml_path.exists():
|
|
raise FileNotFoundError(f"Robot XML not found: {xml_path}")
|
|
|
|
return mujoco.MjModel.from_xml_path(str(xml_path))
|
|
|
|
|
|
def test_unitree_g1_conversion():
|
|
"""Test conversion with Unitree G1 robot."""
|
|
model = load_robot_model("unitree_g1")
|
|
|
|
mesh_geom_count = 0
|
|
has_textures = False
|
|
|
|
for geom_idx in range(model.ngeom):
|
|
if model.geom_type[geom_idx] == mujoco.mjtGeom.mjGEOM_MESH:
|
|
mesh_geom_count += 1
|
|
|
|
# Convert to trimesh.
|
|
mesh = mujoco_mesh_to_trimesh(model, geom_idx)
|
|
|
|
# Basic checks.
|
|
assert mesh is not None, f"Failed to convert geom {geom_idx}"
|
|
assert len(mesh.vertices) > 0, f"Mesh {geom_idx} has no vertices"
|
|
assert len(mesh.faces) > 0, f"Mesh {geom_idx} has no faces"
|
|
|
|
# Check for textures.
|
|
if hasattr(mesh.visual, "uv"):
|
|
has_textures = True
|
|
|
|
assert mesh_geom_count > 0, "No mesh geometries found in Unitree G1"
|
|
print(f"✓ Unitree G1: Successfully converted {mesh_geom_count} mesh geometries")
|
|
if has_textures:
|
|
print(" - Found textured meshes")
|
|
|
|
|
|
def test_unitree_go1_conversion():
|
|
"""Test conversion with Unitree Go1 robot."""
|
|
model = load_robot_model("unitree_go1")
|
|
|
|
mesh_geom_count = 0
|
|
primitive_geom_count = 0
|
|
|
|
for geom_idx in range(model.ngeom):
|
|
geom_type = model.geom_type[geom_idx]
|
|
|
|
if geom_type == mujoco.mjtGeom.mjGEOM_MESH:
|
|
mesh_geom_count += 1
|
|
|
|
# Convert to trimesh.
|
|
mesh = mujoco_mesh_to_trimesh(model, geom_idx)
|
|
|
|
# Basic checks.
|
|
assert mesh is not None, f"Failed to convert geom {geom_idx}"
|
|
assert len(mesh.vertices) > 0, f"Mesh {geom_idx} has no vertices"
|
|
assert len(mesh.faces) > 0, f"Mesh {geom_idx} has no faces"
|
|
else:
|
|
# Count primitive geometries (box, sphere, capsule, etc.).
|
|
primitive_geom_count += 1
|
|
|
|
print(f"✓ Unitree Go1: Successfully converted {mesh_geom_count} mesh geometries")
|
|
print(f" - Also has {primitive_geom_count} primitive geometries")
|
|
|
|
|
|
def test_texture_extraction():
|
|
"""Test texture extraction with a simple textured model."""
|
|
# Create a model with procedural texture.
|
|
xml_string = """
|
|
<mujoco>
|
|
<asset>
|
|
<!-- Procedural checker texture -->
|
|
<texture name="checker" type="2d" builtin="checker" width="64" height="64"
|
|
rgb1="1 0 0" rgb2="0 0 1"/>
|
|
<material name="checker_mat" texture="checker" rgba="1 1 1 1"/>
|
|
|
|
<!-- Simple box mesh with 8 vertices -->
|
|
<mesh name="box"
|
|
vertex="0 0 0 1 0 0 1 1 0 0 1 0 0 0 1 1 0 1 1 1 1 0 1 1"
|
|
face="0 1 2 0 2 3 4 5 6 4 6 7"/>
|
|
</asset>
|
|
|
|
<worldbody>
|
|
<geom name="textured_mesh" type="mesh" mesh="box" material="checker_mat"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
|
|
model = mujoco.MjModel.from_xml_string(xml_string)
|
|
|
|
# Find the mesh geom.
|
|
for geom_idx in range(model.ngeom):
|
|
if model.geom_type[geom_idx] == mujoco.mjtGeom.mjGEOM_MESH:
|
|
mesh = mujoco_mesh_to_trimesh(model, geom_idx)
|
|
|
|
assert mesh is not None, "Failed to convert textured mesh"
|
|
assert mesh.visual is not None, "Mesh has no visual"
|
|
|
|
# Check if texture was extracted (procedural textures should work).
|
|
matid = model.geom_matid[geom_idx]
|
|
if matid >= 0 and matid < model.nmat:
|
|
texid = model.mat_texid[matid]
|
|
# texid might be an array, get the first element if so.
|
|
if hasattr(texid, "__len__"):
|
|
texid = texid[0] if len(texid) > 0 else -1
|
|
if texid >= 0:
|
|
# Should have extracted the checker texture.
|
|
print("✓ Texture extraction: Successfully extracted procedural texture")
|
|
return
|
|
|
|
print("✓ Texture extraction: Tested (no textured meshes in simple model)")
|
|
|
|
|
|
def test_material_colors():
|
|
"""Test that material colors are properly applied."""
|
|
xml_string = """
|
|
<mujoco>
|
|
<asset>
|
|
<material name="red" rgba="1 0 0 1"/>
|
|
<material name="green" rgba="0 1 0 0.5"/>
|
|
<!-- Tetrahedron with 4 vertices (minimum for MuJoCo mesh) -->
|
|
<mesh name="tetra"
|
|
vertex="0 0 0 1 0 0 0.5 0.866 0 0.5 0.289 0.816"
|
|
face="0 1 2 0 1 3 0 2 3 1 2 3"/>
|
|
</asset>
|
|
|
|
<worldbody>
|
|
<geom name="red_mesh" type="mesh" mesh="tetra" material="red"/>
|
|
<geom name="green_mesh" type="mesh" mesh="tetra" material="green" pos="2 0 0"/>
|
|
<geom name="default_mesh" type="mesh" mesh="tetra" pos="4 0 0"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
|
|
model = mujoco.MjModel.from_xml_string(xml_string)
|
|
|
|
colors_found = []
|
|
for geom_idx in range(model.ngeom):
|
|
if model.geom_type[geom_idx] == mujoco.mjtGeom.mjGEOM_MESH:
|
|
mesh = mujoco_mesh_to_trimesh(model, geom_idx)
|
|
|
|
# Check visual colors.
|
|
visual = mesh.visual
|
|
if visual and hasattr(visual, "vertex_colors"):
|
|
# Get the first vertex color (they should all be the same).
|
|
color = visual.vertex_colors[0]
|
|
colors_found.append(tuple(color))
|
|
|
|
assert len(colors_found) == 3, "Should have 3 meshes"
|
|
|
|
# Check that we got different colors.
|
|
assert colors_found[0][:3] == (255, 0, 0), "First mesh should be red"
|
|
assert colors_found[1][:3] == (0, 255, 0), "Second mesh should be green"
|
|
# Third mesh should have default color.
|
|
|
|
print("✓ Material colors: Correctly applied to meshes")
|
|
|
|
|
|
def test_performance():
|
|
"""Test conversion performance with a complex model."""
|
|
import time
|
|
|
|
model = load_robot_model("unitree_g1")
|
|
|
|
mesh_geoms = []
|
|
for geom_idx in range(model.ngeom):
|
|
if model.geom_type[geom_idx] == mujoco.mjtGeom.mjGEOM_MESH:
|
|
mesh_geoms.append(geom_idx)
|
|
|
|
start_time = time.time()
|
|
for geom_idx in mesh_geoms:
|
|
mujoco_mesh_to_trimesh(model, geom_idx)
|
|
elapsed = time.time() - start_time
|
|
|
|
avg_time = elapsed / len(mesh_geoms) * 1000 # Convert to ms.
|
|
print(f"✓ Performance: Converted {len(mesh_geoms)} meshes in {elapsed:.3f}s")
|
|
print(f" - Average: {avg_time:.2f}ms per mesh")
|
|
|
|
# Warn if it's too slow.
|
|
if avg_time > 50:
|
|
print(" ⚠ Warning: Conversion is slow (>50ms per mesh)")
|
|
|
|
|
|
def test_mesh_with_texture_coordinates():
|
|
"""Test meshes with texture coordinates (Issue #225)."""
|
|
xml_string = """
|
|
<mujoco>
|
|
<asset>
|
|
<!-- Simple tetrahedron WITH texture coordinates -->
|
|
<mesh name="textured_tetra"
|
|
vertex="0 0 0 1 0 0 0.5 0.866 0 0.5 0.289 0.816"
|
|
texcoord="0 0 1 0 0.5 1 0.5 0.5"
|
|
face="0 1 2 0 1 3 0 2 3 1 2 3"/>
|
|
</asset>
|
|
<worldbody>
|
|
<geom type="mesh" mesh="textured_tetra"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
|
|
model = mujoco.MjModel.from_xml_string(xml_string)
|
|
|
|
# Verify the model has texture coordinates.
|
|
assert model.mesh_texcoord.shape == (4, 2), "Model should have 4 texture coordinates"
|
|
assert model.mesh_texcoordnum[0] == 4, "Mesh should have 4 texture coordinates"
|
|
|
|
# Convert the mesh.
|
|
mesh = mujoco_mesh_to_trimesh(model, 0)
|
|
|
|
# Verify the mesh was created successfully.
|
|
assert mesh is not None, "Failed to convert textured mesh"
|
|
assert len(mesh.vertices) > 0, "Mesh has no vertices"
|
|
assert len(mesh.faces) > 0, "Mesh has no faces"
|
|
|
|
# With texture coordinates, vertices are duplicated per face.
|
|
# 4 faces * 3 vertices per face = 12 vertices.
|
|
assert len(mesh.vertices) == 12, (
|
|
f"Expected 12 duplicated vertices, got {len(mesh.vertices)}"
|
|
)
|
|
assert len(mesh.faces) == 4, f"Expected 4 faces, got {len(mesh.faces)}"
|
|
|
|
print(
|
|
"✓ Mesh with texture coordinates: Successfully converted mesh with texcoords (Issue #225 fixed)"
|
|
)
|
|
|
|
|
|
def _make_mixed_visual_model() -> mujoco.MjModel:
|
|
"""Create a model with both textured and color-only geoms on the same body."""
|
|
xml = """
|
|
<mujoco>
|
|
<asset>
|
|
<texture name="checker" type="2d" builtin="checker" width="64" height="64"
|
|
rgb1="1 0 0" rgb2="0 0 1"/>
|
|
<material name="tex_mat" texture="checker"/>
|
|
<material name="color_mat" rgba="0 1 0 1"/>
|
|
<mesh name="textured_tetra"
|
|
vertex="0 0 0 1 0 0 0.5 0.866 0 0.5 0.289 0.816"
|
|
texcoord="0 0 1 0 0.5 1 0.5 0.5"
|
|
face="0 1 2 0 1 3 0 2 3 1 2 3"/>
|
|
<mesh name="plain_tetra"
|
|
vertex="2 0 0 3 0 0 2.5 0.866 0 2.5 0.289 0.816"
|
|
face="0 1 2 0 1 3 0 2 3 1 2 3"/>
|
|
</asset>
|
|
<worldbody>
|
|
<body name="mixed">
|
|
<geom name="tex_geom" type="mesh" mesh="textured_tetra" material="tex_mat"/>
|
|
<geom name="color_geom" type="mesh" mesh="plain_tetra" material="color_mat"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
return mujoco.MjModel.from_xml_string(xml)
|
|
|
|
|
|
def test_get_geom_texture_id():
|
|
"""Verify correct texture ID detection for textured vs untextured geoms."""
|
|
model = _make_mixed_visual_model()
|
|
|
|
# Find geom indices by name.
|
|
tex_geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "tex_geom")
|
|
color_geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "color_geom")
|
|
|
|
tex_id = get_geom_texture_id(model, tex_geom_id)
|
|
assert tex_id >= 0, "Textured geom should have a non-negative texture ID"
|
|
|
|
color_id = get_geom_texture_id(model, color_geom_id)
|
|
assert color_id == -1, "Color-only geom should have texture ID -1"
|
|
|
|
|
|
def test_get_geom_texture_id_primitive():
|
|
"""Primitive (non-mesh) geoms should always return -1."""
|
|
xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<geom name="sphere" type="sphere" size="0.1"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
model = mujoco.MjModel.from_xml_string(xml)
|
|
geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "sphere")
|
|
assert get_geom_texture_id(model, geom_id) == -1
|
|
|
|
|
|
def test_group_geoms_by_visual_compat():
|
|
"""Verify geoms are correctly partitioned by visual compatibility."""
|
|
model = _make_mixed_visual_model()
|
|
|
|
tex_geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "tex_geom")
|
|
color_geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "color_geom")
|
|
|
|
groups = group_geoms_by_visual_compat(model, [tex_geom_id, color_geom_id])
|
|
|
|
# Should produce two groups: one textured, one untextured.
|
|
assert len(groups) == 2, f"Expected 2 groups, got {len(groups)}"
|
|
|
|
# Each group should have exactly one geom.
|
|
assert all(len(g) == 1 for g in groups)
|
|
|
|
# Flatten to a set and verify all geoms are present.
|
|
all_ids = {gid for g in groups for gid in g}
|
|
assert all_ids == {tex_geom_id, color_geom_id}
|
|
|
|
|
|
def test_group_geoms_single_type():
|
|
"""All untextured geoms should land in a single group."""
|
|
xml = """
|
|
<mujoco>
|
|
<asset>
|
|
<material name="red" rgba="1 0 0 1"/>
|
|
<material name="blue" rgba="0 0 1 1"/>
|
|
<mesh name="t1"
|
|
vertex="0 0 0 1 0 0 0.5 0.866 0 0.5 0.289 0.816"
|
|
face="0 1 2 0 1 3 0 2 3 1 2 3"/>
|
|
<mesh name="t2"
|
|
vertex="2 0 0 3 0 0 2.5 0.866 0 2.5 0.289 0.816"
|
|
face="0 1 2 0 1 3 0 2 3 1 2 3"/>
|
|
</asset>
|
|
<worldbody>
|
|
<body name="b">
|
|
<geom name="g1" type="mesh" mesh="t1" material="red"/>
|
|
<geom name="g2" type="mesh" mesh="t2" material="blue"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
model = mujoco.MjModel.from_xml_string(xml)
|
|
g1 = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "g1")
|
|
g2 = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "g2")
|
|
|
|
groups = group_geoms_by_visual_compat(model, [g1, g2])
|
|
assert len(groups) == 1, "Two untextured geoms should form a single group"
|
|
assert set(groups[0]) == {g1, g2}
|
|
|
|
|
|
def test_merge_mixed_visual_geoms():
|
|
"""Regression: merging each visual-compat subgroup must preserve visuals."""
|
|
import trimesh.visual
|
|
|
|
model = _make_mixed_visual_model()
|
|
|
|
tex_geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "tex_geom")
|
|
color_geom_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, "color_geom")
|
|
|
|
groups = group_geoms_by_visual_compat(model, [tex_geom_id, color_geom_id])
|
|
|
|
for group in groups:
|
|
mesh = merge_geoms(model, group)
|
|
assert len(mesh.vertices) > 0
|
|
assert len(mesh.faces) > 0
|
|
|
|
tex_id = get_geom_texture_id(model, group[0])
|
|
if tex_id >= 0:
|
|
# Textured group should have TextureVisuals, not default gray.
|
|
assert isinstance(mesh.visual, trimesh.visual.TextureVisuals), (
|
|
f"Expected TextureVisuals for textured group, got {type(mesh.visual)}"
|
|
)
|
|
else:
|
|
# Color group should have ColorVisuals.
|
|
assert isinstance(mesh.visual, trimesh.visual.ColorVisuals), (
|
|
f"Expected ColorVisuals for color group, got {type(mesh.visual)}"
|
|
)
|
|
# Verify the color is green (not default gray).
|
|
vertex_colors = mesh.visual.vertex_colors
|
|
assert vertex_colors[0][1] == 255, (
|
|
f"Expected green channel 255, got {vertex_colors[0][1]}"
|
|
)
|
|
|
|
|
|
def _make_site_model() -> mujoco.MjModel:
|
|
"""Create a model with all 5 site types."""
|
|
xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<body name="test_body" pos="0 0 1">
|
|
<joint type="free"/>
|
|
<geom type="sphere" size="0.01"/>
|
|
<site name="sphere_site" type="sphere" size="0.1" rgba="1 0 0 1"/>
|
|
<site name="capsule_site" type="capsule" size="0.05 0.1" rgba="0 1 0 1"
|
|
pos="1 0 0"/>
|
|
<site name="ellipsoid_site" type="ellipsoid" size="0.1 0.05 0.03"
|
|
rgba="0 0 1 1" pos="2 0 0"/>
|
|
<site name="cylinder_site" type="cylinder" size="0.05 0.1" rgba="1 1 0 1"
|
|
pos="3 0 0"/>
|
|
<site name="box_site" type="box" size="0.1 0.05 0.03" rgba="0 1 1 1"
|
|
pos="4 0 0"/>
|
|
</body>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
return mujoco.MjModel.from_xml_string(xml)
|
|
|
|
|
|
def test_create_site_mesh_sphere():
|
|
"""Test sphere site mesh creation."""
|
|
model = _make_site_model()
|
|
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "sphere_site")
|
|
mesh = create_site_mesh(model, site_id)
|
|
assert len(mesh.vertices) > 0
|
|
assert len(mesh.faces) > 0
|
|
# Check color is red.
|
|
assert mesh.visual is not None
|
|
assert mesh.visual.vertex_colors[0][0] == 255
|
|
assert mesh.visual.vertex_colors[0][1] == 0
|
|
|
|
|
|
def test_create_site_mesh_capsule():
|
|
"""Test capsule site mesh creation."""
|
|
model = _make_site_model()
|
|
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "capsule_site")
|
|
mesh = create_site_mesh(model, site_id)
|
|
assert len(mesh.vertices) > 0
|
|
assert len(mesh.faces) > 0
|
|
# Check color is green.
|
|
assert mesh.visual is not None
|
|
assert mesh.visual.vertex_colors[0][1] == 255
|
|
|
|
|
|
def test_create_site_mesh_ellipsoid():
|
|
"""Test ellipsoid site mesh creation."""
|
|
model = _make_site_model()
|
|
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "ellipsoid_site")
|
|
mesh = create_site_mesh(model, site_id)
|
|
assert len(mesh.vertices) > 0
|
|
assert len(mesh.faces) > 0
|
|
# Check color is blue.
|
|
assert mesh.visual is not None
|
|
assert mesh.visual.vertex_colors[0][2] == 255
|
|
|
|
|
|
def test_create_site_mesh_cylinder():
|
|
"""Test cylinder site mesh creation."""
|
|
model = _make_site_model()
|
|
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "cylinder_site")
|
|
mesh = create_site_mesh(model, site_id)
|
|
assert len(mesh.vertices) > 0
|
|
assert len(mesh.faces) > 0
|
|
|
|
|
|
def test_create_site_mesh_box():
|
|
"""Test box site mesh creation."""
|
|
model = _make_site_model()
|
|
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "box_site")
|
|
mesh = create_site_mesh(model, site_id)
|
|
assert len(mesh.vertices) > 0
|
|
assert len(mesh.faces) > 0
|
|
|
|
|
|
def test_create_site_mesh_rgba_fallback():
|
|
"""Test that all-zero RGBA falls back to gray."""
|
|
xml = """
|
|
<mujoco>
|
|
<worldbody>
|
|
<site name="zero_rgba" type="sphere" size="0.1" rgba="0 0 0 0"/>
|
|
</worldbody>
|
|
</mujoco>
|
|
"""
|
|
model = mujoco.MjModel.from_xml_string(xml)
|
|
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "zero_rgba")
|
|
mesh = create_site_mesh(model, site_id)
|
|
# Should be gray (0.5 * 255 = 127), not transparent black.
|
|
assert mesh.visual is not None
|
|
color = mesh.visual.vertex_colors[0]
|
|
assert color[0] == 127, f"Expected R=127, got {color[0]}"
|
|
assert color[1] == 127, f"Expected G=127, got {color[1]}"
|
|
assert color[2] == 127, f"Expected B=127, got {color[2]}"
|
|
assert color[3] == 255, f"Expected A=255, got {color[3]}"
|
|
|
|
|
|
def test_merge_sites_single():
|
|
"""Test merging a single site returns the site mesh."""
|
|
model = _make_site_model()
|
|
site_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "sphere_site")
|
|
mesh = merge_sites(model, [site_id])
|
|
assert len(mesh.vertices) > 0
|
|
assert len(mesh.faces) > 0
|
|
|
|
|
|
def test_merge_sites_multiple():
|
|
"""Test merging multiple sites combines them correctly."""
|
|
model = _make_site_model()
|
|
sphere_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "sphere_site")
|
|
capsule_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "capsule_site")
|
|
box_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, "box_site")
|
|
|
|
single_sphere = create_site_mesh(model, sphere_id)
|
|
single_capsule = create_site_mesh(model, capsule_id)
|
|
single_box = create_site_mesh(model, box_id)
|
|
|
|
merged = merge_sites(model, [sphere_id, capsule_id, box_id])
|
|
|
|
# Merged mesh should have at least the sum of individual vertices.
|
|
expected_min_verts = (
|
|
len(single_sphere.vertices)
|
|
+ len(single_capsule.vertices)
|
|
+ len(single_box.vertices)
|
|
)
|
|
assert len(merged.vertices) >= expected_min_verts, (
|
|
f"Expected >= {expected_min_verts} vertices, got {len(merged.vertices)}"
|
|
)
|
|
assert len(merged.faces) > 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run all tests
|
|
print("=" * 60)
|
|
print("Testing Viser Conversions")
|
|
print("=" * 60)
|
|
|
|
tests = [
|
|
test_unitree_g1_conversion,
|
|
test_unitree_go1_conversion,
|
|
test_texture_extraction,
|
|
test_material_colors,
|
|
test_performance,
|
|
test_mesh_with_texture_coordinates,
|
|
test_get_geom_texture_id,
|
|
test_get_geom_texture_id_primitive,
|
|
test_group_geoms_by_visual_compat,
|
|
test_group_geoms_single_type,
|
|
test_merge_mixed_visual_geoms,
|
|
]
|
|
|
|
failed = []
|
|
for test in tests:
|
|
try:
|
|
test()
|
|
except Exception as e:
|
|
test_name = test.__name__ if hasattr(test, "__name__") else str(test)
|
|
print(f"✗ {test_name}: {e}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
failed.append(test_name)
|
|
|
|
print("\n" + "=" * 60)
|
|
if failed:
|
|
print(f"Failed tests: {', '.join(failed)}")
|
|
import sys
|
|
|
|
sys.exit(1)
|
|
else:
|
|
print("All tests passed!")
|
|
import sys
|
|
|
|
sys.exit(0)
|