Skip to content

Commit 287f606

Browse files
kunni918duburcqa
authored andcommitted
Raise on ambiguous heterogeneous morph access
1 parent 36fe292 commit 287f606

6 files changed

Lines changed: 134 additions & 5 deletions

File tree

genesis/engine/entities/rigid_entity/rigid_entity.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1951,6 +1951,16 @@ def n_links(self):
19511951
"""The number of `RigidLink` in the entity."""
19521952
return len(self._links)
19531953

1954+
@property
1955+
def morph(self):
1956+
"""The morph of the entity."""
1957+
if self._enable_heterogeneous:
1958+
gs.raise_exception(
1959+
"Heterogeneous entities have multiple morph variants. Use `.morphs` for all variants, "
1960+
"or `.main_morph` only when explicitly using the first variant."
1961+
)
1962+
return self._morph
1963+
19541964
@property
19551965
def main_morph(self):
19561966
"""The main morph of the entity (first morph for heterogeneous entities)."""

genesis/ext/pyrender/overlay/plugin.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,12 @@ def _capture_pending_entities_kwargs(self):
373373
editor panel. Keyed by entity name; values are the kwargs forwarded to ``scene.add_entity``."""
374374
self._pending_entities_kwargs = {}
375375
for entity in self.scene.entities:
376-
kwargs: dict[str, Any] = {"morph": entity.morph}
376+
# Heterogeneous entities raise on .morph; capture all variants so the rebuild reproduces them.
377+
if isinstance(entity, gs.engine.entities.KinematicEntity) and entity._enable_heterogeneous:
378+
morph = tuple(entity.morphs)
379+
else:
380+
morph = entity.morph
381+
kwargs: dict[str, Any] = {"morph": morph}
377382
# Carry the material and surface so a rebuild preserves the entity's solver (e.g. a Kinematic entity must
378383
# not silently become Rigid, the add_entity default). visualize_contact is rigid-only.
379384
if isinstance(entity, gs.engine.entities.KinematicEntity):

genesis/vis/rasterizer_context.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,8 +446,8 @@ def on_rigid(self):
446446
# For z-axis normal planes, render a single instance shared across all envs to avoid z-fighting,
447447
# unless they do not overlap.
448448
env_shared = not self.env_separate_rigid
449-
if not env_shared and isinstance(entity.morph, gs.morphs.Plane):
450-
plane_normal, plane_size = entity.morph.normal, entity.morph.plane_size
449+
if not env_shared and isinstance(entity.main_morph, gs.morphs.Plane):
450+
plane_normal, plane_size = entity.main_morph.normal, entity.main_morph.plane_size
451451
if (
452452
abs(plane_normal[0]) < gs.EPS
453453
and abs(plane_normal[1]) < gs.EPS
@@ -565,8 +565,8 @@ def update_rigid(self):
565565
geom_T = geoms_T[geom.idx][geom_envs_idx]
566566

567567
# Keep single-instance for z-axis normal planes (see on_rigid)
568-
if isinstance(entity.morph, gs.morphs.Plane):
569-
plane_normal, plane_size = entity.morph.normal, entity.morph.plane_size
568+
if isinstance(entity.main_morph, gs.morphs.Plane):
569+
plane_normal, plane_size = entity.main_morph.normal, entity.main_morph.plane_size
570570
if (
571571
abs(plane_normal[0]) < gs.EPS
572572
and abs(plane_normal[1]) < gs.EPS

tests/test_imgui_overlay.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,45 @@
2020
_IMGUI_BUNDLE_AVAILABLE = False
2121

2222

23+
@pytest.mark.required
24+
def test_imgui_overlay_capture_pending_entities_preserves_heterogeneous_morphs():
25+
scene = gs.Scene(show_viewer=False)
26+
single_morph = gs.morphs.Box(size=(0.1, 0.1, 0.1))
27+
single_entity = scene.add_entity(
28+
morph=single_morph,
29+
visualize_contact=True,
30+
name="single",
31+
)
32+
heterogeneous_morphs = (
33+
gs.morphs.Box(size=(0.2, 0.2, 0.2)),
34+
gs.morphs.Cylinder(radius=0.05, height=0.2),
35+
)
36+
heterogeneous_entity = scene.add_entity(
37+
morph=heterogeneous_morphs,
38+
visualize_contact=True,
39+
name="heterogeneous",
40+
)
41+
42+
plugin = ImGuiOverlayPlugin.__new__(ImGuiOverlayPlugin)
43+
plugin.scene = scene
44+
45+
plugin._capture_pending_entities_kwargs()
46+
47+
single_kwargs = plugin._pending_entities_kwargs[single_entity.name]
48+
assert single_kwargs["morph"] is single_morph
49+
assert single_kwargs["material"] is single_entity.material
50+
assert single_kwargs["surface"] is single_entity.surface
51+
assert single_kwargs["visualize_contact"] is True
52+
53+
heterogeneous_kwargs = plugin._pending_entities_kwargs[heterogeneous_entity.name]
54+
assert heterogeneous_kwargs["morph"] == heterogeneous_morphs
55+
assert heterogeneous_kwargs["morph"][0] is heterogeneous_morphs[0]
56+
assert heterogeneous_kwargs["morph"][1] is heterogeneous_morphs[1]
57+
assert heterogeneous_kwargs["material"] is heterogeneous_entity.material
58+
assert heterogeneous_kwargs["surface"] is heterogeneous_entity.surface
59+
assert heterogeneous_kwargs["visualize_contact"] is True
60+
61+
2362
def _apply_deterministic_imgui_overrides(monkeypatch):
2463
"""Make ImGui rendering and timing pixel-identical across renderers for snapshot tests."""
2564
from imgui_bundle import imgui

tests/test_render.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ def skip_if_not_installed(renderer_type):
7979
pytest.skip(SKIP_NO_LUISA)
8080

8181

82+
@pytest.mark.required
83+
@pytest.mark.parametrize("renderer_type", [RENDERER_TYPE.RASTERIZER])
84+
def test_rasterizer_context_heterogeneous_main_morph_no_throw(monkeypatch, renderer):
85+
from genesis.vis.rasterizer import Rasterizer
86+
87+
def _skip_offscreen_renderer_build(self):
88+
self.visualizer = self._context.visualizer
89+
90+
monkeypatch.setattr(Rasterizer, "build", _skip_offscreen_renderer_build)
91+
92+
scene = gs.Scene(
93+
renderer=renderer,
94+
vis_options=gs.options.VisOptions(
95+
env_separate_rigid=True,
96+
shadow=False,
97+
),
98+
show_viewer=False,
99+
)
100+
scene.add_entity(
101+
morph=(
102+
gs.morphs.Box(size=(0.1, 0.1, 0.1)),
103+
gs.morphs.Cylinder(radius=0.05, height=0.2),
104+
),
105+
)
106+
scene.add_entity(
107+
morph=(
108+
gs.morphs.Box(size=(0.2, 0.2, 0.2)),
109+
gs.morphs.Sphere(radius=0.1),
110+
),
111+
material=gs.materials.Kinematic(),
112+
)
113+
114+
scene.build(n_envs=2)
115+
scene.visualizer.context.update(force_render=True)
116+
117+
82118
@pytest.mark.required
83119
@pytest.mark.parametrize(
84120
"renderer_type",

tests/test_rigid_physics.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6998,6 +6998,45 @@ def test_heterogeneous_invalid_material_raises():
69986998

69996999

70007000
@pytest.mark.slow # ~200s
7001+
@pytest.mark.required
7002+
def test_heterogeneous_morph_property_raises():
7003+
"""Test that heterogeneous entities require explicit access through morphs."""
7004+
scene = gs.Scene(show_viewer=False)
7005+
7006+
single_morph = gs.morphs.Box(size=(0.1, 0.1, 0.1))
7007+
single_obj = scene.add_entity(morph=single_morph)
7008+
7009+
rigid_morphs_heterogeneous = (
7010+
gs.morphs.Box(size=(0.1, 0.1, 0.1)),
7011+
gs.morphs.Cylinder(radius=0.05, height=0.2),
7012+
)
7013+
rigid_obj = scene.add_entity(morph=rigid_morphs_heterogeneous)
7014+
kinematic_morphs_heterogeneous = (
7015+
gs.morphs.Box(size=(0.2, 0.2, 0.2)),
7016+
gs.morphs.Sphere(radius=0.1),
7017+
)
7018+
kinematic_obj = scene.add_entity(
7019+
morph=kinematic_morphs_heterogeneous,
7020+
material=gs.materials.Kinematic(),
7021+
)
7022+
7023+
assert single_obj.morph is single_morph
7024+
assert rigid_obj.main_morph is rigid_morphs_heterogeneous[0]
7025+
assert list(rigid_obj.morphs) == list(rigid_morphs_heterogeneous)
7026+
with pytest.raises(gs.GenesisException, match=r"Heterogeneous.*\.morphs") as exc_info:
7027+
_ = rigid_obj.morph
7028+
assert ".main_morph" in str(exc_info.value)
7029+
7030+
assert kinematic_obj.main_morph is kinematic_morphs_heterogeneous[0]
7031+
assert list(kinematic_obj.morphs) == list(kinematic_morphs_heterogeneous)
7032+
with pytest.raises(gs.GenesisException, match=r"Heterogeneous.*\.morphs"):
7033+
_ = kinematic_obj.morph
7034+
7035+
repr(rigid_obj)
7036+
repr(kinematic_obj)
7037+
repr(scene.entities)
7038+
7039+
70017040
@pytest.mark.required
70027041
def test_heterogeneous_fewer_envs_than_variants():
70037042
"""Test that having fewer environments than variants works correctly.

0 commit comments

Comments
 (0)