See #6501. Fix bug where wall alignment didn't strictly check for poll requirements prior to executing operator.

This commit is contained in:
Dion Moult
2025-04-21 15:38:26 +10:00
parent 5af2c3914a
commit 2db6b860b2
7 changed files with 264 additions and 83 deletions
+4 -41
View File
@@ -577,49 +577,12 @@ class AlignProduct(bpy.types.Operator):
align_type: AlignType align_type: AlignType
def execute(self, context): def execute(self, context):
selected_objs = context.selected_objects try:
if len(selected_objs) < 2 or not context.active_object: core.align_objects(tool.Blender, tool.Model, self.align_type)
self.report({"ERROR"}, "Please select atleast 2 objects.") except core.RequireAtLeastTwoElements as e:
return {"FINISHED"} self.report({"ERROR"}, str(e))
if self.align_type == "CENTERLINE":
point = context.active_object.matrix_world @ (
Vector(context.active_object.bound_box[0]) + (context.active_object.dimensions / 2)
)
elif self.align_type == "POSITIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[6])
elif self.align_type == "NEGATIVE":
point = context.active_object.matrix_world @ Vector(context.active_object.bound_box[0])
else:
assert_never(self.align_type)
active_x_axis = context.active_object.matrix_world.to_quaternion() @ Vector((1, 0, 0))
active_y_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 1, 0))
active_z_axis = context.active_object.matrix_world.to_quaternion() @ Vector((0, 0, 1))
x_distances = self.get_axis_distances(point, active_x_axis, context)
y_distances = self.get_axis_distances(point, active_y_axis, context)
if abs(sum(x_distances)) < abs(sum(y_distances)):
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_x_axis * -x_distances[i]) @ obj.matrix_world
else:
for i, obj in enumerate(selected_objs):
obj.matrix_world = Matrix.Translation(active_y_axis * -y_distances[i]) @ obj.matrix_world
return {"FINISHED"} return {"FINISHED"}
def get_axis_distances(self, point: Vector, axis: Vector, context: bpy.types.Context) -> list[float]:
results = []
for obj in context.selected_objects:
if self.align_type == "CENTERLINE":
obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
elif self.align_type == "POSITIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
elif self.align_type == "NEGATIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[0])
else:
assert_never(self.align_type)
results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis))
return results
class LoadTypeThumbnails(bpy.types.Operator): class LoadTypeThumbnails(bpy.types.Operator):
bl_idname = "bim.load_type_thumbnails" bl_idname = "bim.load_type_thumbnails"
+11 -29
View File
@@ -140,31 +140,11 @@ class AlignWall(bpy.types.Operator):
if TYPE_CHECKING: if TYPE_CHECKING:
align_type: AlignType align_type: AlignType
@classmethod
def poll(cls, context):
if not context.active_object:
cls.poll_message_set("No active object selected.")
return False
selected_valid_objects = tool.Model.get_selected_mesh_objects()
if len(selected_valid_objects) < 2:
cls.poll_message_set("Please select at least two mesh objects.")
return False
return True
def execute(self, context): def execute(self, context):
selected_objects = tool.Model.get_selected_mesh_objects() try:
for obj in selected_objects: core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), self.align_type)
if obj == context.active_object: except core.RequireAtLeastTwoLayeredElements as e:
continue self.report({"ERROR"}, str(e))
aligner = DumbWallAligner(obj, context.active_object)
if self.align_type == "CENTERLINE":
aligner.align_centerline()
elif self.align_type == "EXTERIOR":
aligner.align_first_layer()
elif self.align_type == "INTERIOR":
aligner.align_last_layer()
else:
assert_never(self.align_type)
return {"FINISHED"} return {"FINISHED"}
@@ -613,11 +593,11 @@ class DumbWallAligner:
# An alignment shifts the origin of all walls to the closest point on the # An alignment shifts the origin of all walls to the closest point on the
# local X axis of the reference wall. In addition, the Z rotation is copied. # local X axis of the reference wall. In addition, the Z rotation is copied.
# Z translations are ignored for alignment. # Z translations are ignored for alignment.
def __init__(self, wall: bpy.types.Object, reference_wall: bpy.types.Object): def set_reference_wall(self, reference_wall: bpy.types.Object):
self.wall = wall
self.reference_wall = reference_wall self.reference_wall = reference_wall
def align_centerline(self) -> None: def align_centerline(self, wall: bpy.types.Object) -> None:
self.wall = wall
self.align_rotation() self.align_rotation()
l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5) l_start = Vector(self.reference_wall.bound_box[0]).lerp(Vector(self.reference_wall.bound_box[3]), 0.5)
@@ -635,7 +615,8 @@ class DumbWallAligner:
new_origin = point - offset new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_last_layer(self) -> None: def align_last_layer(self, wall: bpy.types.Object) -> None:
self.wall = wall
self.align_rotation() self.align_rotation()
if self.is_rotation_flipped(): if self.is_rotation_flipped():
@@ -658,7 +639,8 @@ class DumbWallAligner:
new_origin = point - offset new_origin = point - offset
self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy self.wall.matrix_world.translation[0], self.wall.matrix_world.translation[1] = new_origin.xy
def align_first_layer(self) -> None: def align_first_layer(self, wall: bpy.types.Object) -> None:
self.wall = wall
self.align_rotation() self.align_rotation()
if self.is_rotation_flipped(): if self.is_rotation_flipped():
@@ -23,7 +23,7 @@ import bpy.utils.previews
import bonsai.bim import bonsai.bim
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.core.model as core import bonsai.core.model as core
from bonsai.bim.module.model.wall import DumbWallJoiner from bonsai.bim.module.model.wall import DumbWallJoiner, DumbWallAligner
from bonsai.bim.helper import prop_with_search, draw_attribute from bonsai.bim.helper import prop_with_search, draw_attribute
from bpy.types import WorkSpaceTool, Menu from bpy.types import WorkSpaceTool, Menu
from bonsai.bim.module.model.data import AuthoringData, ItemData from bonsai.bim.module.model.data import AuthoringData, ItemData
@@ -1181,10 +1181,15 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
return return
if self.active_material_usage == "LAYER2": if self.active_material_usage == "LAYER2":
if bpy.ops.bim.align_wall.poll(): try:
bpy.ops.bim.align_wall(align_type="CENTERLINE") core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "CENTERLINE")
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
else: else:
bpy.ops.bim.align_product(align_type="CENTERLINE") try:
core.align_objects(tool.Blender, tool.Model, "CENTERLINE")
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
def hotkey_S_E(self): def hotkey_S_E(self):
if not bpy.context.selected_objects or not (active_object := bpy.context.active_object): if not bpy.context.selected_objects or not (active_object := bpy.context.active_object):
@@ -1328,18 +1333,30 @@ class Hotkey(bpy.types.Operator, tool.Ifc.Operator):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
return return
elif self.active_material_usage == "LAYER2": elif self.active_material_usage == "LAYER2":
bpy.ops.bim.align_wall(align_type="INTERIOR") try:
core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "INTERIOR")
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
else: else:
bpy.ops.bim.align_product(align_type="POSITIVE") try:
core.align_objects(tool.Blender, tool.Model, "POSITIVE")
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
def hotkey_S_X(self): def hotkey_S_X(self):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
return return
if self.active_material_usage == "LAYER2": if self.active_material_usage == "LAYER2":
if bpy.ops.bim.align_wall.poll(): try:
bpy.ops.bim.align_wall(align_type="EXTERIOR") core.align_walls(tool.Ifc, tool.Blender, tool.Model, DumbWallAligner(), "EXTERIOR")
except core.RequireAtLeastTwoLayeredElements as e:
self.report({"ERROR"}, str(e))
else: else:
bpy.ops.bim.align_product(align_type="NEGATIVE") try:
core.align_objects(tool.Blender, tool.Model, "NEGATIVE")
except core.RequireAtLeastTwoElements as e:
self.report({"ERROR"}, str(e))
def hotkey_S_Y(self): def hotkey_S_Y(self):
if not bpy.context.selected_objects: if not bpy.context.selected_objects:
+46 -1
View File
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
import bonsai.tool as tool import bonsai.tool as tool
from mathutils import Vector from mathutils import Vector
from bonsai.bim.module.model.wall import DumbWallJoiner from bonsai.bim.module.model.wall import DumbWallJoiner, DumbWallAligner
def unjoin_walls( def unjoin_walls(
@@ -82,6 +82,43 @@ def join_walls_LV(
joiner.connect(another_selected_object, active_obj) joiner.connect(another_selected_object, active_obj)
def align_walls(
ifc: tool.Ifc,
blender: tool.Blender,
model: tool.Model,
aligner: DumbWallAligner,
align_type: Literal["CENTERLINE", "EXTERIOR", "INTERIOR"],
):
reference_obj = blender.get_active_object(is_selected=True)
if not (e := ifc.get_entity(reference_obj) or not model.get_usage_type(e) == "LAYER2"):
reference_obj = None
objs = [
o
for o in blender.get_selected_objects()
if o != reference_obj and (e := ifc.get_entity(o)) and model.get_usage_type(e) == "LAYER2"
]
if not reference_obj or not objs:
raise RequireAtLeastTwoLayeredElements(
"At least two vertically layered elements must be selected to match alignments."
)
aligner.set_reference_wall(reference_obj)
for obj in objs:
if align_type == "CENTERLINE":
aligner.align_centerline(obj)
elif align_type == "EXTERIOR":
aligner.align_first_layer(obj)
elif align_type == "INTERIOR":
aligner.align_last_layer(obj)
def align_objects(blender: tool.Blender, model: tool.Model, align_type: Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]):
reference_obj = blender.get_active_object(is_selected=True)
objs = [o for o in blender.get_selected_objects() if o != reference_obj]
if not reference_obj or not objs:
raise RequireAtLeastTwoElements("At least two objects must be selected to match alignments.")
model.align_objects(reference_obj, objs, align_type)
def extend_wall_to_slab( def extend_wall_to_slab(
ifc: tool.Ifc, ifc: tool.Ifc,
geometry: tool.Geometry, geometry: tool.Geometry,
@@ -145,3 +182,11 @@ class RequireTwoWallsError(Exception):
class RequireAtLeastTwoLayeredElements(Exception): class RequireAtLeastTwoLayeredElements(Exception):
pass pass
class RequireAtLeastTwoElements(Exception):
pass
class RequireLayeredElement(Exception):
pass
+43
View File
@@ -39,6 +39,7 @@ import ifcopenshell.util.unit
import bonsai.core.geometry import bonsai.core.geometry
import bonsai.core.tool import bonsai.core.tool
import bonsai.tool as tool import bonsai.tool as tool
import mathutils
from math import atan, cos, degrees, pi, radians from math import atan, cos, degrees, pi, radians
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
from copy import deepcopy from copy import deepcopy
@@ -2325,3 +2326,45 @@ class Model(bonsai.core.tool.Model):
assert isinstance(group_node, bpy.types.GeometryNodeGroup) assert isinstance(group_node, bpy.types.GeometryNodeGroup)
return [s for s in group_node.inputs if s.type != "GEOMETRY"] return [s for s in group_node.inputs if s.type != "GEOMETRY"]
@classmethod
def align_objects(cls, reference_obj: bpy.types.Object, objs: Iterable[bpy.types.Object], align_type: Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]):
if align_type == "CENTERLINE":
point = reference_obj.matrix_world @ (
Vector(reference_obj.bound_box[0]) + (reference_obj.dimensions / 2)
)
elif align_type == "POSITIVE":
point = reference_obj.matrix_world @ Vector(reference_obj.bound_box[6])
elif align_type == "NEGATIVE":
point = reference_obj.matrix_world @ Vector(reference_obj.bound_box[0])
reference_x_axis = reference_obj.matrix_world.col[0].to_3d()
reference_y_axis = reference_obj.matrix_world.col[1].to_3d()
x_distances = cls.get_axis_distances(point, reference_x_axis, objs, align_type)
y_distances = cls.get_axis_distances(point, reference_y_axis, objs, align_type)
if abs(sum(x_distances)) < abs(sum(y_distances)):
for i, obj in enumerate(objs):
obj.matrix_world = Matrix.Translation(reference_x_axis * -x_distances[i]) @ obj.matrix_world
else:
for i, obj in enumerate(objs):
obj.matrix_world = Matrix.Translation(reference_y_axis * -y_distances[i]) @ obj.matrix_world
@classmethod
def get_axis_distances(cls, point: Vector, axis: Vector, objs: Iterable[bpy.types.Object], align_type: Literal["CENTERLINE", "POSITIVE", "NEGATIVE"]) -> list[float]:
results = []
for obj in objs:
if align_type == "CENTERLINE":
obj_point = obj.matrix_world @ (Vector(obj.bound_box[0]) + (obj.dimensions / 2))
elif align_type == "POSITIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[6])
elif align_type == "NEGATIVE":
obj_point = obj.matrix_world @ Vector(obj.bound_box[0])
results.append(mathutils.geometry.distance_point_to_plane(obj_point, point, axis))
return results
@classmethod
def offset_wall(cls, wall: bpy.types.Object, baseline: Literal["EXTERIOR", "INTERIOR", "CENTERLINE"]) -> None:
element = tool.Ifc.get_entity(wall)
if baseline == "CENTERLINE":
pass
+131
View File
@@ -286,6 +286,137 @@ Scenario: Split a wall which has a flipped door
And I press "bim.hotkey(hotkey='S_K')" And I press "bim.hotkey(hotkey='S_K')"
Then the object "IfcDoor/Door" is at "8.01,0.1,0" Then the object "IfcDoor/Door" is at "8.01,0.1,0"
Scenario: Align two walls - centerline
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL300'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And the cursor is at "10,5,0"
And I press "bim.add_occurrence"
When the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
And I press "bim.hotkey(hotkey='S_C')"
Then the object "IfcWall/Wall" bottom left corner is at "0,0,0"
And the object "IfcWall/Wall" top right corner is at "1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "10,-0.1,0"
And the object "IfcWall/Wall.001" top right corner is at "11,0.2,3"
Scenario: Align two walls - interior
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL300'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And the cursor is at "10,5,0"
And I press "bim.add_occurrence"
When the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
And I press "bim.hotkey(hotkey='S_V')"
Then the object "IfcWall/Wall" bottom left corner is at "0,0,0"
And the object "IfcWall/Wall" top right corner is at "1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "10,-0.2,0"
And the object "IfcWall/Wall.001" top right corner is at "11,0.1,3"
Scenario: Align two walls - exterior
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL300'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And the cursor is at "10,5,0"
And I press "bim.add_occurrence"
When the object "IfcWall/Wall.001" is selected
And additionally the object "IfcWall/Wall" is selected
And I press "bim.hotkey(hotkey='S_X')"
Then the object "IfcWall/Wall" bottom left corner is at "0,0,0"
And the object "IfcWall/Wall" top right corner is at "1,0.1,3"
And the object "IfcWall/Wall.001" bottom left corner is at "10,0,0"
And the object "IfcWall/Wall.001" top right corner is at "11,0.3,3"
Scenario: Align two walls - centerline fail due to selection criteria
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcWallType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcWallType') if e.Name == 'WAL100'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
When the object "IfcWall/Wall" is selected
Then I press "bim.hotkey(hotkey='S_C')" and expect error "Error: At least two vertically layered elements must be selected to match alignments."
Scenario: Align two elements - centerline
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the cursor is at "10,5,0"
And I press "bim.add_occurrence"
And the object "IfcDoor/Door.001" is rotated by "0,0,90" deg
When the object "IfcDoor/Door.001" is selected
And additionally the object "IfcDoor/Door" is selected
And I press "bim.hotkey(hotkey='S_C')"
Then the object "IfcDoor/Door" bottom left corner is at "0,0,0"
And the object "IfcDoor/Door" top right corner is at "1.01,0.1,2.145"
And the object "IfcDoor/Door.001" bottom left corner is at "10,-0.455,0"
And the object "IfcDoor/Door.001" top right corner is at "9.9,0.555,2.145"
Scenario: Align two elements - positive
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the cursor is at "10,5,0"
And I press "bim.add_occurrence"
And the object "IfcDoor/Door.001" is rotated by "0,0,90" deg
When the object "IfcDoor/Door.001" is selected
And additionally the object "IfcDoor/Door" is selected
And I press "bim.hotkey(hotkey='S_V')"
Then the object "IfcDoor/Door" bottom left corner is at "0,0,0"
And the object "IfcDoor/Door" top right corner is at "1.01,0.1,2.145"
And the object "IfcDoor/Door.001" bottom left corner is at "10,-0.910,0"
And the object "IfcDoor/Door.001" top right corner is at "9.9,0.1,2.145"
Scenario: Align two elements - negative
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
And the cursor is at "10,5,0"
And I press "bim.add_occurrence"
And the object "IfcDoor/Door.001" is rotated by "0,0,90" deg
When the object "IfcDoor/Door.001" is selected
And additionally the object "IfcDoor/Door" is selected
And I press "bim.hotkey(hotkey='S_X')"
Then the object "IfcDoor/Door" bottom left corner is at "0,0,0"
And the object "IfcDoor/Door" top right corner is at "1.01,0.1,2.145"
And the object "IfcDoor/Door.001" bottom left corner is at "10,0,0"
And the object "IfcDoor/Door.001" top right corner is at "9.9,1.01,2.145"
Scenario: Align elements - fail due to selection criteria
Given an empty IFC project
And I load the demo construction library
And I set "scene.BIMModelProperties.ifc_class" to "IfcDoorType"
And the variable "element_type" is "[e for e in {ifc}.by_type('IfcDoorType') if e.Name == 'DT01'][0].id()"
And I set "scene.BIMModelProperties.relating_type_id" to "{element_type}"
And I press "bim.add_occurrence"
When the object "IfcDoor/Door" is selected
Then I press "bim.hotkey(hotkey='S_C')" and expect error "Error: At least two objects must be selected to match alignments."
Scenario: Add a slab Scenario: Add a slab
Given an empty IFC project Given an empty IFC project
And I load the demo construction library And I load the demo construction library
+3 -3
View File
@@ -1264,7 +1264,7 @@ def the_object_name_is_at_location(name, location):
obj_location = the_object_name_exists(name).location obj_location = the_object_name_exists(name).location
assert ( assert (
obj_location - Vector([float(co) for co in location.split(",")]) obj_location - Vector([float(co) for co in location.split(",")])
).length < 0.1, f"Object is at {obj_location} instead of {location}" ).length < 0.05, f"Object is at {obj_location} instead of {location}"
@then(parsers.parse('the object "{name}" has a vertex at "{location}"')) @then(parsers.parse('the object "{name}" has a vertex at "{location}"'))
@@ -1305,7 +1305,7 @@ def the_object_name_top_right_corner_is_at_location(name, location):
obj_corner = obj.matrix_world @ Vector(obj.bound_box[6]) obj_corner = obj.matrix_world @ Vector(obj.bound_box[6])
assert ( assert (
obj_corner - Vector([float(co) for co in location.split(",")]) obj_corner - Vector([float(co) for co in location.split(",")])
).length < 0.1, f"Object has top right corner {obj_corner} instead of {location}" ).length < 0.05, f"Object has top right corner {obj_corner} instead of {location}"
@then(parsers.parse('the object "{name}" bottom left corner is at "{location}"')) @then(parsers.parse('the object "{name}" bottom left corner is at "{location}"'))
@@ -1314,7 +1314,7 @@ def the_object_name_bottom_left_corner_is_at_location(name, location):
obj_corner = obj.matrix_world @ Vector(obj.bound_box[0]) obj_corner = obj.matrix_world @ Vector(obj.bound_box[0])
assert ( assert (
obj_corner - Vector([float(co) for co in location.split(",")]) obj_corner - Vector([float(co) for co in location.split(",")])
).length < 0.1, f"Object has bottom left corner {obj_corner} instead of {location}" ).length < 0.05, f"Object has bottom left corner {obj_corner} instead of {location}"
@then(parsers.parse('the object "{name}" is contained in "{container_name}"')) @then(parsers.parse('the object "{name}" is contained in "{container_name}"'))