diff --git a/docs/glossary.rst b/docs/glossary.rst index 4a6c7de61..e0c5dee79 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -47,7 +47,7 @@ Glossary footprint The infinite extrusion of a 2D `region` in the positive and negative Z directions. Testing containment of an `object` in a 2D region automatically uses its footprint, so that the object is considered contained if and only if its projection into the plane of the region is contained in the region. - Footprints are represented internally by instances of the `PolygonalFootprintRegion` class. + Footprints are represented internally by instances of the `PolygonalFootprintRegion` class, and can be accessed using the ``footprint`` attribute. global parameters Parameters of a scene like weather or time of day which are not associated with any object. diff --git a/docs/reference/operators.rst b/docs/reference/operators.rst index 5c4df83f1..de6038f4f 100644 --- a/docs/reference/operators.rst +++ b/docs/reference/operators.rst @@ -67,6 +67,14 @@ See the :ref:`Visibility System ` reference for a discussion of the ---------------------------------- Whether a position or `Object` lies in the `Region`; for the latter, the object must be completely contained in the region. +.. _({Object} | {region}) intersects ({Object} | {region}): + +(*Object* | *region*) intersects (*Object* | *region*) +------------------------------------------------------ +Whether an `Object`/`Region` intersects another `Object`/`Region`, i.e. whether any portion of the occupied spaces intersect. + +When working with 2D regions, it can be useful to check intersection with the :term:`footprint` of a region, e.g. when checking whether a car intersects a given lane. In this case, one would write :scenic:`car intersects lane.footprint` instead of :scenic:`car intersects lane`. For more details, see :term:`footprint`. + Orientation Operators ===================== diff --git a/docs/syntax_guide.rst b/docs/syntax_guide.rst index c376ad450..c017a9cdb 100644 --- a/docs/syntax_guide.rst +++ b/docs/syntax_guide.rst @@ -284,9 +284,11 @@ In the following tables, operators are grouped by the type of value they return. * - Boolean Operators - Meaning * - :sampref:`({Point} | {OrientedPoint}) can see ({vector} | {Object})` - - Whether or not a position or `Object` is visible from a `Point` or `OrientedPoint`. + - Whether or not a position or `Object` is visible from a `Point` or `OrientedPoint` * - :sampref:`({vector} | {Object}) in {region}` - - Whether a position or `Object` lies in the region + - Whether a position or `Object` lies in the region + * - :sampref:`({Object} | {region}) intersects ({Object} | {region})` + - Whether an `Object`/`Region` intersects an `Object`/`Region`. .. list-table:: diff --git a/src/scenic/core/object_types.py b/src/scenic/core/object_types.py index cb1c9b93f..702f4a47d 100644 --- a/src/scenic/core/object_types.py +++ b/src/scenic/core/object_types.py @@ -1117,9 +1117,16 @@ def distanceTo(self, point): @cached_method def intersects(self, other): - """Whether or not this object intersects another object""" - # For objects that are boxes and flat, we can take a fast route - if self._isPlanarBox and other._isPlanarBox: + """Whether or not this object intersects another object or region""" + ## Type Checking ## + if not isinstance(other, (Object, Region)): + raise TypeError( + f"Cannot compute intersection of Scenic Object with {type(other)}." + ) + + ## Heuristic Fast Paths ## + # For two objects that are boxes and flat, we can take a fast route + if self._isPlanarBox and (isinstance(other, Object) and other._isPlanarBox): if abs(self.position.z - other.position.z) > (self.height + other.height) / 2: return False @@ -1127,12 +1134,27 @@ def intersects(self, other): other_poly = other._boundingPolygon return self_poly.intersects(other_poly) - if isLazy(self.occupiedSpace) or isLazy(other.occupiedSpace): + # For an object that is a box and flat with a polygonal region, we can + # also take a fast route. + if self._isPlanarBox and ( + isinstance(other, PolygonalRegion) + and abs(self.position.z - other.z) <= self.height / 2 + ): + return self._boundingPolygon.intersects(other.polygons) + + ## Default Case + # Extract other's occupied space if it's an object + if isinstance(other, Object): + other_occupied_space = other.occupiedSpace + else: + other_occupied_space = other + + if isLazy(self.occupiedSpace) or isLazy(other_occupied_space): raise RandomControlFlowError( "Cannot compute intersection between Objects with non-fixed values." ) - return self.occupiedSpace.intersects(other.occupiedSpace) + return self.occupiedSpace.intersects(other_occupied_space) @cached_property def left(self): diff --git a/src/scenic/syntax/ast.py b/src/scenic/syntax/ast.py index 57e5779ef..e64e3be48 100644 --- a/src/scenic/syntax/ast.py +++ b/src/scenic/syntax/ast.py @@ -1250,3 +1250,13 @@ def __init__(self, left: ast.AST, right: ast.AST, *args: any, **kwargs: any) -> self.left = left self.right = right self._fields = ["left", "right"] + + +class IntersectsOp(AST): + __match_args__ = ("left", "right") + + def __init__(self, left: ast.AST, right: ast.AST, *args: any, **kwargs: any) -> None: + super().__init__(*args, **kwargs) + self.left = left + self.right = right + self._fields = ["left", "right"] diff --git a/src/scenic/syntax/compiler.py b/src/scenic/syntax/compiler.py index ea0417735..3c5a1c7ef 100644 --- a/src/scenic/syntax/compiler.py +++ b/src/scenic/syntax/compiler.py @@ -1746,3 +1746,13 @@ def visit_CanSeeOp(self, node: s.CanSeeOp): ], keywords=[], ) + + def visit_IntersectsOp(self, node: s.IntersectsOp): + return ast.Call( + func=ast.Name(id="Intersects", ctx=loadCtx), + args=[ + self.visit(node.left), + self.visit(node.right), + ], + keywords=[], + ) diff --git a/src/scenic/syntax/scenic.gram b/src/scenic/syntax/scenic.gram index 2c0e64783..76af206a8 100644 --- a/src/scenic/syntax/scenic.gram +++ b/src/scenic/syntax/scenic.gram @@ -1573,6 +1573,7 @@ bitwise_or: | scenic_visible_from | scenic_not_visible_from | scenic_can_see + | scenic_intersects | a=bitwise_or '|' b=bitwise_xor { ast.BinOp(left=a, op=ast.BitOr(), right=b, LOCATIONS) } | bitwise_xor @@ -1582,6 +1583,8 @@ scenic_not_visible_from: a=bitwise_or "not" "visible" 'from' b=bitwise_xor { s.N scenic_can_see: a=bitwise_or "can" "see" b=bitwise_xor { s.CanSeeOp(left=a, right=b, LOCATIONS) } +scenic_intersects: a=bitwise_or "intersects" b=bitwise_xor { s.IntersectsOp(left=a, right=b, LOCATIONS) } + bitwise_xor: | scenic_offset_along | a=bitwise_xor '^' b=bitwise_and { ast.BinOp(left=a, op=ast.BitXor(), right=b, LOCATIONS) } diff --git a/src/scenic/syntax/veneer.py b/src/scenic/syntax/veneer.py index 51fd84918..730745220 100644 --- a/src/scenic/syntax/veneer.py +++ b/src/scenic/syntax/veneer.py @@ -79,6 +79,7 @@ "RelativeTo", "OffsetAlong", "CanSee", + "Intersects", "Until", "Implies", "VisibleFromOp", @@ -1367,6 +1368,15 @@ def canSeeHelper(X, Y, objects): return canSeeHelper(X, Y, objects) +@distributionFunction +def Intersects(X, Y): + """The :scenic:`{X} intersects {Y}` operator.""" + if isA(X, Object): + return X.intersects(Y) + else: + return Y.intersects(X) + + ### Specifiers diff --git a/tests/syntax/test_compiler.py b/tests/syntax/test_compiler.py index fe3e0fe85..1af4dbf9a 100644 --- a/tests/syntax/test_compiler.py +++ b/tests/syntax/test_compiler.py @@ -2154,6 +2154,14 @@ def test_can_see_op(self): case _: assert False + def test_intersects_op(self): + node, _ = compileScenicAST(IntersectsOp(Name("X"), Name("Y"))) + match node: + case Call(Name("Intersects"), [Name("X"), Name("Y")]): + assert True + case _: + assert False + # Test cases inherited from the old translator for checking edge cases diff --git a/tests/syntax/test_operators.py b/tests/syntax/test_operators.py index a3e0b732c..13d499d4d 100644 --- a/tests/syntax/test_operators.py +++ b/tests/syntax/test_operators.py @@ -456,7 +456,7 @@ def test_point_in_region_2d(): ptA = new Point at 11@4.5 ptB = new Point at 11@3.5 ptC = new Point at (11, 4.5, 1) - param p = tuple([9@5.5 in reg, 9@7 in reg, (11, 4.5, -1) in reg, ptA in reg, ptB in reg, ptC in reg]) + param p = (9@5.5 in reg, 9@7 in reg, (11, 4.5, -1) in reg, ptA in reg, ptB in reg, ptC in reg) """ ) assert p == (True, False, True, True, False, True) @@ -469,7 +469,7 @@ def test_object_in_region_2d(): ego = new Object at 11.5@5.5, with width 0.25, with length 0.25 other_1 = new Object at 9@4.5, with width 2.5 other_2 = new Object at (11.5, 5.5, 2), with width 0.25, with length 0.25 - param p = tuple([ego in reg, other_1 in reg, other_2 in reg]) + param p = (ego in reg, other_1 in reg, other_2 in reg) """ ) assert p == (True, False, True) @@ -482,7 +482,7 @@ def test_point_in_region_3d(): reg = BoxRegion() ptA = new Point at (0.25,0.25,0.25) ptB = new Point at (1,1,1) - param p = tuple([(0,0,0) in reg, (0.49,0.49,0.49) in reg, (0.5,0.5,0.5) in reg, (0.51,0.51,0.51) in reg, ptA in reg, ptB in reg]) + param p = ((0,0,0) in reg, (0.49,0.49,0.49) in reg, (0.5,0.5,0.5) in reg, (0.51,0.51,0.51) in reg, ptA in reg, ptB in reg) """ ) assert p == (True, True, True, False, True, False) @@ -497,12 +497,111 @@ def test_object_in_region_3d(): obj_2 = new Object at (0.49, 0.49, 0.49), with allowCollisions True obj_3 = new Object at (0.75, 0.75, 0.75), with allowCollisions True obj_4 = new Object at (3,3,3), with allowCollisions True - param p = tuple([obj_1 in reg, obj_2 in reg, obj_3 in reg, obj_4 in reg]) + param p = (obj_1 in reg, obj_2 in reg, obj_3 in reg, obj_4 in reg) """ ) assert p == (True, True, False, False) +# Intersects +def test_intersects_obj_obj(): + p = sampleParamPFrom( + """ + obj1 = new Object at (-1,0,0.1), with allowCollisions True + obj2 = new Object at (1,0,0), with allowCollisions True + obj3 = new Object with width 10, with length 10, with height 10, with allowCollisions True + param p = (obj1 intersects obj2, obj1 intersects obj3, obj2 intersects obj3) + """ + ) + assert p == (False, True, True) + + # Case where neither corners or centers intersect, but + # Objects still intersect. + p = sampleParamPFrom( + """ + obj1 = new Object at (10,0,0), with width 30, with allowCollisions True + obj2 = new Object at (0,10,0), with length 30, with allowCollisions True + param p = (obj1 intersects obj2, + obj1.position in obj2.occupiedSpace, obj2.position in obj1.occupiedSpace, + any((c in obj2.occupiedSpace) for c in obj1.corners), + any((c in obj1.occupiedSpace) for c in obj2.corners)) + """ + ) + assert p == (True, False, False, False, False) + + +def test_intersects_region_region(): + p = sampleParamPFrom( + """ + reg1 = BoxRegion(position=(-1,0,0.1)) + reg2 = BoxRegion(position=(1,0,0)) + reg3 = BoxRegion(dimensions=(10,10,10)) + param p = (reg1 intersects reg2, reg1 intersects reg3, reg2 intersects reg3) + """ + ) + assert p == (False, True, True) + + +def test_intersects_obj_region(): + p = sampleParamPFrom( + """ + reg1 = BoxRegion(position=(-1,0,0.1)) + obj2 = new Object at (1,0,0), with allowCollisions True + obj3 = new Object with width 10, with length 10, with height 10, with allowCollisions True + param p = (reg1 intersects obj2, obj2 intersects reg1, + reg1 intersects obj3, obj3 intersects reg1) + """ + ) + assert p == (False, False, True, True) + + +def test_intersects_2d(): + p = sampleParamPFrom( + """ + obj1 = new Object at (0.2,0,0), with allowCollisions True + obj2 = new Object at (-0.2,0,0), with allowCollisions True + reg = RectangularRegion((0,0,0), 0, 10, 10) + param p = (obj1 intersects obj2, obj1 intersects reg) + """ + ) + assert p == (True, True) + + +def test_intersects_non_0_z(): + p = sampleParamPFrom( + """ + obj1 = new Object at (0.2,0,1), with allowCollisions True + obj2 = new Object at (-0.2,0,1), with allowCollisions True + reg = RectangularRegion((0,0,1), 0, 10, 10) + param p = (obj1 intersects obj2, obj1 intersects reg) + """ + ) + assert p == (True, True) + + +def test_intersects_overlap(): + p = sampleParamPFrom( + """ + obj = new Object at (0,0,0), with allowCollisions True + reg = RectangularRegion((0.5,0,0), 0, 1, 1) + param p = obj intersects reg + """ + ) + assert p == True + + +def test_intersects_diff_z(): + p = sampleParamPFrom( + """ + obj1 = new Object at (0,0,0.1), with allowCollisions True + obj2 = new Object at (0,0,10), with allowCollisions True + reg = RectangularRegion((0,0,0), 0, 10, 10) + param p = (obj1 intersects reg, obj2 intersects reg, obj1 intersects obj2) + """ + ) + assert p == (True, False, False) + + ## Heading operators diff --git a/tests/syntax/test_parser.py b/tests/syntax/test_parser.py index 65f2cd940..da5f80545 100644 --- a/tests/syntax/test_parser.py +++ b/tests/syntax/test_parser.py @@ -2845,3 +2845,12 @@ def test_can_see(self): assert True case _: assert False + + def test_intersects(self): + mod = parse_string_helper("x intersects y ") + stmt = mod.body[0] + match stmt: + case Expr(IntersectsOp(Name("x"), Name("y"))): + assert True + case _: + assert False