From f5c715c797516a55ab83ba74584ad7dfa05d73d1 Mon Sep 17 00:00:00 2001 From: karhankayan Date: Wed, 28 Aug 2024 21:27:06 -0400 Subject: [PATCH] check coplanar, add test case --- .../constraint_language/relations.py | 6 ++ .../example_solver/geometry/dof.py | 8 ++ .../example_solver/geometry/stability.py | 45 ++++++++++ .../example_solver/geometry/validity.py | 13 ++- tests/solver/test_stable_against.py | 82 ++++++++++++++++++- 5 files changed, 152 insertions(+), 2 deletions(-) diff --git a/infinigen/core/constraints/constraint_language/relations.py b/infinigen/core/constraints/constraint_language/relations.py index feb581b6d..223fc8033 100644 --- a/infinigen/core/constraints/constraint_language/relations.py +++ b/infinigen/core/constraints/constraint_language/relations.py @@ -390,11 +390,17 @@ class SupportedBy(Touching): class CoPlanar(GeometryRelation): margin: float = 0 + # rev_normal: if True, align the normals so they face the SAME direction, rather than two planes facing eachother. + # typical use is for sink embedded in countertop + rev_normal: bool = False + __repr__ = no_frozenset_repr @dataclass(frozen=True) class StableAgainst(GeometryRelation): + margin: float = 0 + # check_ if False, only check x/z stability, z is allowed to overhand. # typical use is chair-against-table relation check_z: bool = True diff --git a/infinigen/core/constraints/example_solver/geometry/dof.py b/infinigen/core/constraints/example_solver/geometry/dof.py index 48a0c7594..e3612722c 100644 --- a/infinigen/core/constraints/example_solver/geometry/dof.py +++ b/infinigen/core/constraints/example_solver/geometry/dof.py @@ -109,6 +109,11 @@ def rotate_object_around_axis(obj, axis, std, angle=None): def check_init_valid( state: state_def.State, name: str, obj_planes: list, assigned_planes: list, margins ): + """ + Check that the plane assignments to the object is valid. First checks that the rotations can be satisfied, then + checks that the translations can be satisfied. Returns a boolean indicating if the assignments are valid, the number + of degrees of freedom remaining, and the translation vector if the assignments are valid. + """ if len(obj_planes) == 0: raise ValueError(f"{check_init_valid.__name__} for {name=} got {obj_planes=}") if len(obj_planes) > 3: @@ -117,6 +122,9 @@ def check_init_valid( ) def get_rot(ind): + """ + Get the rotation axis and angle needed to align the object's plane with the assigned plane. + """ try: a = obj_planes[ind][0] b = assigned_planes[ind][0] diff --git a/infinigen/core/constraints/example_solver/geometry/stability.py b/infinigen/core/constraints/example_solver/geometry/stability.py index c42b28b1d..f9f8e1524 100644 --- a/infinigen/core/constraints/example_solver/geometry/stability.py +++ b/infinigen/core/constraints/example_solver/geometry/stability.py @@ -177,6 +177,51 @@ def stable_against( return True +@gin.configurable +def coplanar( + state: state_def.State, + obj_name: str, + relation_state: state_def.RelationState, +): + """ + check that the object's tagged surface is coplanar with the target object's tagged surface translated with margin. + """ + + relation = relation_state.relation + assert isinstance(relation, cl.CoPlanar) + + logger.debug(f"coplanar {obj_name=} {relation_state=}") + a_blender_obj = state.objs[obj_name].obj + b_blender_obj = state.objs[relation_state.target_name].obj + + pa, pb = state.planes.get_rel_state_planes(state, obj_name, relation_state) + + poly_a = state.planes.planerep_to_poly(pa) + poly_b = state.planes.planerep_to_poly(pb) + + normal_a = iu.global_polygon_normal(a_blender_obj, poly_a) + normal_b = iu.global_polygon_normal(b_blender_obj, poly_b) + dot = np.array(normal_a).dot(normal_b) + if not (np.isclose(np.abs(dot), 1, atol=1e-2) or np.isclose(dot, -1, atol=1e-2)): + logger.debug(f"coplanar failed, not parallel {dot=}") + return False + + origin_b = iu.global_vertex_coordinates( + b_blender_obj, b_blender_obj.data.vertices[poly_b.vertices[0]] + ) + + for vertex in poly_a.vertices: + vertex_global = iu.global_vertex_coordinates( + a_blender_obj, a_blender_obj.data.vertices[vertex] + ) + distance = iu.distance_to_plane(vertex_global, origin_b, normal_b) + if not np.isclose(distance, relation_state.relation.margin, atol=1e-2): + logger.debug(f"coplanar failed, not close to {distance=}") + return False + + return True + + def snap_against(scene, a, b, a_plane, b_plane, margin=0): """ snap a against b with some margin. diff --git a/infinigen/core/constraints/example_solver/geometry/validity.py b/infinigen/core/constraints/example_solver/geometry/validity.py index a38f8697c..e348fcc21 100644 --- a/infinigen/core/constraints/example_solver/geometry/validity.py +++ b/infinigen/core/constraints/example_solver/geometry/validity.py @@ -19,7 +19,10 @@ any_touching, constrain_contact, ) -from infinigen.core.constraints.example_solver.geometry.stability import stable_against +from infinigen.core.constraints.example_solver.geometry.stability import ( + coplanar, + stable_against, +) from infinigen.core.constraints.example_solver.state_def import State from infinigen.core.util import blender as butil @@ -76,6 +79,14 @@ def all_relations_valid(state, name): f"{name} failed relation {i=}/{len(rels)} {relation_state.relation} on {relation_state.target_name}" ) return False + + case cl.CoPlanar(_child_tags, _parent_tags, _margin): + res = coplanar(state, name, relation_state) + if not res: + logger.debug( + f"{name} failed relation {i=}/{len(rels)} {relation_state.relation} on {relation_state.target_name}" + ) + return False case _: raise TypeError(f"Unhandled {relation_state.relation}") diff --git a/tests/solver/test_stable_against.py b/tests/solver/test_stable_against.py index a02cd3e84..7bac0b34c 100644 --- a/tests/solver/test_stable_against.py +++ b/tests/solver/test_stable_against.py @@ -52,6 +52,50 @@ def make_scene(loc2): return state_def.State(objs=objs) +def make_scene_coplanar(loc2): + """Create a scene with a table and a cup, and return the state.""" + butil.clear_scene() + objs = {} + + table = butil.spawn_cube(scale=(5, 5, 1), name="table") + cup = butil.spawn_cube(scale=(1, 1, 1), name="cup", location=loc2) + + for o in [table, cup]: + butil.apply_transform(o) + parse_scene.preprocess_obj(o) + tagging.tag_canonical_surfaces(o) + + assert table.scale == Vector((1, 1, 1)) + assert cup.location != Vector((0, 0, 0)) + + bpy.context.view_layer.update() + + objs["table"] = state_def.ObjectState(table) + objs["cup"] = state_def.ObjectState(cup) + objs["cup"].relations.append( + state_def.RelationState( + cl.StableAgainst({t.Subpart.Bottom}, {t.Subpart.Top}), + target_name="table", + child_plane_idx=0, + parent_plane_idx=0, + ) + ) + back = {t.Subpart.Back, -t.Subpart.Top, -t.Subpart.Front} + back_coplanar_back = cl.CoPlanar(back, back, margin=0) + + objs["cup"].relations.append( + state_def.RelationState( + back_coplanar_back, + target_name="table", + child_plane_idx=0, + parent_plane_idx=0, + ) + ) + butil.save_blend("test.blend") + + return state_def.State(objs=objs) + + def test_stable_against(): # too low, intersects ground assert not validity.check_post_move_validity(make_scene((0, 0, 0.5)), "cup") @@ -150,5 +194,41 @@ def test_horizontal_stability(): # butil.save_blend('test.blend') +def test_coplanar(): + # Test case 1: Cup is stable against but not coplanar (should be invalid) + assert not validity.check_post_move_validity(make_scene_coplanar((0, 0, 1)), "cup") + + # Test case 2: Cup is stable against and coplanar with the table (should be valid) + assert validity.check_post_move_validity(make_scene_coplanar((-2, 0, 1)), "cup") + + # Test case 3: Cup is coplanar but not stable against (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((-5.2, 0, 1)), "cup" + ) + + # Test case 4: Cup is neither stable against nor coplanar (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((2, 2, 1.1)), "cup" + ) + + # Test case 5: Cup is at the back edge, stable against and coplanar (should be valid) + assert validity.check_post_move_validity(make_scene_coplanar((-2, 2, 1)), "cup") + + # Test case 6: Cup is slightly off the back edge, not stable against but coplanar (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((-2.1, 2, 1)), "cup" + ) + + # Test case 7: Cup is far from the table (should be invalid) + assert not validity.check_post_move_validity( + make_scene_coplanar((10, 10, 10)), "cup" + ) + + # Test case 8: Cup is inside the table, not stable against but coplanar (should be invalid) + assert not validity.check_post_move_validity(make_scene_coplanar((-2, 0, 0)), "cup") + + print("All test cases for coplanar constraint passed successfully.") + + if __name__ == "__main__": - test_horizontal_stability() + test_coplanar()