Skip to content

Commit f5a74a0

Browse files
authored
ObjectPlacer: reject overlapping placements (#439)
## Summary `_validate_placement` was a stub that always returned True, so the solver could converge to solutions where objects overlap - Implement `_validate_placement` in ObjectPlacer to detect bounding-box overlaps between placed objects, causing the placer to retry again.
1 parent 1f62bd6 commit f5a74a0

File tree

4 files changed

+132
-12
lines changed

4 files changed

+132
-12
lines changed

isaaclab_arena/relations/object_placer.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,18 +103,20 @@ def place(
103103
if self.params.verbose:
104104
print(f"Attempt {attempt + 1}/{self.params.max_placement_attempts}: loss = {loss:.6f}")
105105

106-
# Track best result
107-
if loss < best_loss:
108-
best_loss = loss
109-
best_positions = positions
110-
111106
# Check if placement is valid
112107
if self._validate_placement(positions):
108+
best_loss = loss
109+
best_positions = positions
113110
success = True
114111
if self.params.verbose:
115112
print(f"Success on attempt {attempt + 1}")
116113
break
117114

115+
# Track best invalid result as fallback
116+
if loss < best_loss:
117+
best_loss = loss
118+
best_positions = positions
119+
118120
# Apply solved positions to objects
119121
if self.params.apply_positions_to_objects:
120122
self._apply_positions(best_positions, anchor_objects_set)
@@ -187,18 +189,29 @@ def _validate_placement(
187189
self,
188190
positions: dict[Object | ObjectReference, tuple[float, float, float]],
189191
) -> bool:
190-
"""Validate that the placement is geometrically valid.
192+
"""Validate that no two objects overlap in 3D.
193+
194+
Checks all object pairs for axis-aligned bounding box overlap.
191195
192196
Args:
193-
positions: Dictionary mapping objects to their positions.
197+
positions: Dictionary mapping objects to their solved (x, y, z) positions.
194198
195199
Returns:
196-
True if placement is valid, False otherwise.
200+
True if no overlaps exist, False otherwise.
197201
"""
198-
# TODO(cvolk): Implement geometric checks like:
199-
# - Collision detection between objects
200-
# - Boundary checks (objects within workspace)
201-
print("WARNING: Placement validation not yet implemented. Skipping geometric checks (collision, boundary).")
202+
objects = list(positions.keys())
203+
for i in range(len(objects)):
204+
for j in range(i + 1, len(objects)):
205+
a, b = objects[i], objects[j]
206+
207+
a_world = a.get_bounding_box().translated(positions[a])
208+
b_world = b.get_bounding_box().translated(positions[b])
209+
210+
if a_world.overlaps(b_world, margin=self.params.min_separation_m):
211+
if self.params.verbose:
212+
print(f" Overlap between '{a.name}' and '{b.name}'")
213+
return False
214+
202215
return True
203216

204217
def _apply_positions(

isaaclab_arena/relations/object_placer_params.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,8 @@ class ObjectPlacerParams:
3333

3434
placement_seed: int | None = None
3535
"""Random seed for reproducible placement. If None, uses current RNG state."""
36+
37+
min_separation_m: float = 0.0
38+
"""Minimum separation (meters) required between object bounding boxes.
39+
Set to 0.0 to only reject actual overlaps. A small positive value (e.g. 0.005)
40+
adds a safety margin between objects."""
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright (c) 2025-2026, The Isaac Lab Arena Project Developers (https://github.com/isaac-sim/IsaacLab-Arena/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
"""Tests for ObjectPlacer._validate_placement 3D overlap detection."""
7+
8+
from isaaclab_arena.assets.dummy_object import DummyObject
9+
from isaaclab_arena.relations.object_placer import ObjectPlacer
10+
from isaaclab_arena.relations.object_placer_params import ObjectPlacerParams
11+
from isaaclab_arena.utils.bounding_box import AxisAlignedBoundingBox
12+
13+
14+
def _make_box(name: str, size: float = 0.2) -> DummyObject:
15+
half = size / 2
16+
return DummyObject(
17+
name=name,
18+
bounding_box=AxisAlignedBoundingBox(min_point=(-half, -half, -half), max_point=(half, half, half)),
19+
)
20+
21+
22+
def _make_desk() -> DummyObject:
23+
return DummyObject(
24+
name="desk",
25+
bounding_box=AxisAlignedBoundingBox(min_point=(-0.5, -0.5, 0.0), max_point=(0.5, 0.5, 0.05)),
26+
)
27+
28+
29+
def test_no_overlap_returns_true():
30+
"""Two boxes far apart should pass validation."""
31+
placer = ObjectPlacer(params=ObjectPlacerParams())
32+
a = _make_box("a")
33+
b = _make_box("b")
34+
positions = {a: (0.0, 0.0, 0.0), b: (1.0, 0.0, 0.0)}
35+
assert placer._validate_placement(positions) is True
36+
37+
38+
def test_overlapping_returns_false():
39+
"""Two boxes at the same position should fail validation."""
40+
placer = ObjectPlacer(params=ObjectPlacerParams())
41+
a = _make_box("a")
42+
b = _make_box("b")
43+
positions = {a: (0.0, 0.0, 0.0), b: (0.0, 0.0, 0.0)}
44+
assert placer._validate_placement(positions) is False
45+
46+
47+
def test_partial_overlap_returns_false():
48+
"""Two boxes with partial 3D overlap should fail."""
49+
placer = ObjectPlacer(params=ObjectPlacerParams())
50+
a = _make_box("a", size=0.2)
51+
b = _make_box("b", size=0.2)
52+
positions = {a: (0.0, 0.0, 0.0), b: (0.1, 0.1, 0.0)}
53+
assert placer._validate_placement(positions) is False
54+
55+
56+
def test_separated_in_z_passes():
57+
"""Two boxes sharing XY footprint but separated in Z should pass."""
58+
placer = ObjectPlacer(params=ObjectPlacerParams())
59+
a = _make_box("a")
60+
b = _make_box("b")
61+
positions = {a: (0.0, 0.0, 0.0), b: (0.0, 0.0, 5.0)}
62+
assert placer._validate_placement(positions) is True
63+
64+
65+
def test_object_on_surface_no_overlap():
66+
"""A box placed above a desk surface (no 3D overlap) should pass."""
67+
placer = ObjectPlacer(params=ObjectPlacerParams())
68+
desk = _make_desk()
69+
box = _make_box("box", size=0.2)
70+
# Desk top at z=0.05; box at z=0.16 → box occupies z=[0.06, 0.26], clear of desk
71+
positions = {desk: (0.0, 0.0, 0.0), box: (0.0, 0.0, 0.16)}
72+
assert placer._validate_placement(positions) is True
73+
74+
75+
def test_colocated_siblings_overlap_rejected():
76+
"""Two objects at the same position should fail."""
77+
placer = ObjectPlacer(params=ObjectPlacerParams())
78+
desk = _make_desk()
79+
a = _make_box("a", size=0.2)
80+
b = _make_box("b", size=0.2)
81+
positions = {desk: (0.0, 0.0, 0.0), a: (0.0, 0.0, 0.15), b: (0.0, 0.0, 0.15)}
82+
assert placer._validate_placement(positions) is False

isaaclab_arena/utils/bounding_box.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,26 @@ def centered(self) -> "AxisAlignedBoundingBox":
154154
),
155155
)
156156

157+
def overlaps(self, other: "AxisAlignedBoundingBox", margin: float = 0.0) -> bool:
158+
"""Check if two AABBs overlap in 3D.
159+
160+
Args:
161+
other: The other bounding box to test against.
162+
margin: Minimum required separation in meters. A positive value
163+
rejects placements where the gap is smaller than margin.
164+
165+
Returns:
166+
True if the volumes overlap (or are closer than margin).
167+
"""
168+
return (
169+
self.max_point[0] + margin > other.min_point[0]
170+
and other.max_point[0] + margin > self.min_point[0]
171+
and self.max_point[1] + margin > other.min_point[1]
172+
and other.max_point[1] + margin > self.min_point[1]
173+
and self.max_point[2] + margin > other.min_point[2]
174+
and other.max_point[2] + margin > self.min_point[2]
175+
)
176+
157177
def rotated_90_around_z(self, quarters: int) -> "AxisAlignedBoundingBox":
158178
"""Rotate AABB by quarters * 90° around Z axis.
159179

0 commit comments

Comments
 (0)