diff --git a/src/bonsai/Makefile b/src/bonsai/Makefile index d7e799ae0d..6fc51c463a 100644 --- a/src/bonsai/Makefile +++ b/src/bonsai/Makefile @@ -362,7 +362,7 @@ endif .PHONY: test-modal test-modal: - blender --enable-event-simulate --python test/modal/test_modal.py + blender --enable-event-simulate --python test/modal/test_modal.py --window-maximized # Reregistering test is not added to the standard test suite because during unregister # Blender removes all Bonsai dependencies breaking dev-environment symlinks. diff --git a/src/bonsai/test/files/snap.ifc b/src/bonsai/test/files/snap.ifc index b99cf6c6c2..763e873985 100644 --- a/src/bonsai/test/files/snap.ifc +++ b/src/bonsai/test/files/snap.ifc @@ -1,7 +1,7 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition[DesignTransferView]'),'2;1'); -FILE_NAME('snap.ifc','2026-04-28T11:17:26-03:00',(),(),'IfcOpenShell 0.0.0','Bonsai 0.8.6-alpha260415-29fe41e','Nobody'); +FILE_NAME('snap.ifc','2026-05-31T21:32:39-03:00',(),(),'IfcOpenShell 0.0.0','Bonsai 0.8.6-alpha260430-a712367','Nobody'); FILE_SCHEMA(('IFC4')); ENDSEC; DATA; @@ -1144,7 +1144,7 @@ DATA; #1220=IFCREPRESENTATIONMAP(#1207,#1219); #1221=IFCTYPEPRODUCT('0klFX9AjnEnPBkdIURv8XD',$,'NAME-TAG',$,'IfcAnnotation/TEXT',(#1203),(#1220),$); #1222=IFCWALL('3fphKxC81BMwRc46o$1Cqj',$,'Wall_02',$,$,#1336,#1230,$,$); -#1223=IFCRELCONTAINEDINSPATIALSTRUCTURE('2EB4R7ETPDiw0Rbe6drZly',$,$,$,(#1486,#1320),#42); +#1223=IFCRELCONTAINEDINSPATIALSTRUCTURE('2EB4R7ETPDiw0Rbe6drZly',$,$,$,(#1513,#1486,#1320,#1545),#42); #1224=IFCRELDEFINESBYTYPE('0jBmsPz3v5HvFHMoyoj824',$,$,$,(#1222,#1249),#71); #1225=IFCMATERIALLAYERSETUSAGE(#74,.AXIS2.,.POSITIVE.,0.,$); #1226=IFCRELASSOCIATESMATERIAL('0q0BFzauPCjBOVG64mRZDH',$,$,$,(#1222),#1225); @@ -1225,5 +1225,44 @@ DATA; #1510=IFCDIRECTION((1.,0.,0.)); #1511=IFCAXIS2PLACEMENT3D(#1508,#1509,#1510); #1512=IFCLOCALPLACEMENT(#65,#1511); +#1513=IFCSLAB('0VbnYWxhj2CvlANx9odXFl',$,'Slab',$,$,#1521,#1528,$,$); +#1514=IFCRELDEFINESBYTYPE('2gPl7v_sfDchsCMRc9DNak',$,$,$,(#1513),#108); +#1515=IFCMATERIALLAYERSETUSAGE(#111,.AXIS3.,.POSITIVE.,-200.,$); +#1516=IFCRELASSOCIATESMATERIAL('3e$Ju5XVL2YhZkvCqEpR4x',$,$,$,(#1513),#1515); +#1517=IFCCARTESIANPOINT((-3.13916466154751E-05,100.000001490116,0.)); +#1518=IFCDIRECTION((0.,0.,1.)); +#1519=IFCDIRECTION((1.,0.,0.)); +#1520=IFCAXIS2PLACEMENT3D(#1517,#1518,#1519); +#1521=IFCLOCALPLACEMENT(#65,#1520); +#1522=IFCCARTESIANPOINTLIST2D(((0.,0.),(0.00754979009798262,-100000.),(100000.007629395,-99999.9847412109),(99999.9847412109,0.0137314200401306),(0.,0.))); +#1523=IFCINDEXEDPOLYCURVE(#1522,$,$); +#1524=IFCDIRECTION((0.,0.,1.)); +#1525=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#1523); +#1526=IFCEXTRUDEDAREASOLID(#1525,#1535,#1524,200.); +#1527=IFCSHAPEREPRESENTATION(#15,'Body','SweptSolid',(#1526)); +#1528=IFCPRODUCTDEFINITIONSHAPE($,$,(#1527)); +#1529=IFCPROPERTYSET('28pnRt0NXCjOi1lCnPN6KX',$,'EPset_Parametric',$,(#1531)); +#1530=IFCRELDEFINESBYPROPERTIES('0nMlPeDT5D$OSy1tx9_6Ob',$,$,$,(#1513),#1529); +#1531=IFCPROPERTYSINGLEVALUE('Engine',$,IFCLABEL('Bonsai.DumbLayer3'),$); +#1532=IFCCARTESIANPOINT((-0.,-0.,-200.)); +#1533=IFCDIRECTION((0.,0.,1.)); +#1534=IFCDIRECTION((1.,0.,0.)); +#1535=IFCAXIS2PLACEMENT3D(#1532,#1533,#1534); +#1536=IFCCARTESIANPOINTLIST3D(((0.,0.,0.),(0.,0.,1999.99987792969),(0.,1999.99987792969,0.),(0.,1999.99987792969,1999.99987792969),(1999.99987792969,0.,0.),(1999.99987792969,0.,1999.99987792969),(1999.99987792969,1999.99987792969,0.),(1999.99987792969,1999.99987792969,1999.99987792969))); +#1537=IFCINDEXEDPOLYGONALFACE((1,2,4,3)); +#1538=IFCINDEXEDPOLYGONALFACE((3,4,8,7)); +#1539=IFCINDEXEDPOLYGONALFACE((7,8,6,5)); +#1540=IFCINDEXEDPOLYGONALFACE((5,6,2,1)); +#1541=IFCINDEXEDPOLYGONALFACE((3,7,5,1)); +#1542=IFCINDEXEDPOLYGONALFACE((8,4,2,6)); +#1543=IFCPOLYGONALFACESET(#1536,$,(#1537,#1538,#1539,#1540,#1541,#1542),$); +#1544=IFCSHAPEREPRESENTATION(#15,'Body','Tessellation',(#1543)); +#1545=IFCBUILDINGELEMENTPROXY('1vHaLvW0jCZfGnicRbz_9o',$,'Cube',$,$,#1551,#1546,$,.COMPLEX.); +#1546=IFCPRODUCTDEFINITIONSHAPE($,$,(#1544)); +#1547=IFCCARTESIANPOINT((1000000.06103516,1000000.06103516,0.)); +#1548=IFCDIRECTION((0.,0.,1.)); +#1549=IFCDIRECTION((1.,0.,0.)); +#1550=IFCAXIS2PLACEMENT3D(#1547,#1548,#1549); +#1551=IFCLOCALPLACEMENT(#65,#1550); ENDSEC; END-ISO-10303-21; diff --git a/src/bonsai/test/modal/test_modal.py b/src/bonsai/test/modal/test_modal.py index 4c88c1903f..5adca02647 100644 --- a/src/bonsai/test/modal/test_modal.py +++ b/src/bonsai/test/modal/test_modal.py @@ -20,6 +20,7 @@ import inspect import os import sys +import time import bpy import ifcopenshell @@ -113,21 +114,25 @@ def new_project(): props.template_file = "0" tool.Blender.get_addon_preferences().should_play_chaching_sound = False +def get_area_and_region(window): + area = next(area for area in window.screen.areas if area.type == "VIEW_3D") + region = next(region for region in area.regions if region.type == "WINDOW") + return area, region + +def test_snap_object_detection(window): + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", 0, 0) + area, region = get_area_and_region(window) + x = round(area.width * 0.5 + area.x) + y = round(area.height * 0.54 + area.y) -def test_snap_object_detection(window, x, y): yield from preset_event_simulate(window, "ESC", "TAP", x, y) measure_settings = tool.Project.get_measure_tool_settings() measure_settings.measurement_type = "POLYLINE" for obj in tool.Blender.get_selected_objects(): obj.select_set(False) - for area in bpy.context.screen.areas: - if area.type == "VIEW_3D": - for region in area.regions: - if region.type == "WINDOW": - with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): - bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type="POLYLINE") - break + with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): + bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type="POLYLINE") yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x, y) @@ -156,9 +161,66 @@ def test_snap_object_detection(window, x, y): yield from preset_event_simulate(window, "RET", "TAP", x, y) yield "FINISHED" +def test_snap_partially_behind_camera(window): + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", 0, 0) + area, region = get_area_and_region(window) + x = round(area.width * 0.20 + area.x) + y = round(area.height * 0.15 + area.y) + + yield from preset_event_simulate(window, "ESC", "TAP", x, y) + + measure_settings = tool.Project.get_measure_tool_settings() + measure_settings.measurement_type = "POLYLINE" + for obj in tool.Blender.get_selected_objects(): + obj.select_set(False) + with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): + bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type="POLYLINE") + + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) + yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x, y) + snap_point = tool.Model.get_polyline_props().snap_mouse_point[0] + assert_msg = "First click should have a snap_object" + assert snap_point.snap_object, assert_msg + _assert_pass(assert_msg) + assert_msg = "snap_object should be a string with the object name" + assert type(snap_point.snap_object) == str, assert_msg + _assert_pass(assert_msg) + assert_msg = "snap_type should be 'Edge'" + assert snap_point.snap_type == "Edge", assert_msg + _assert_pass(assert_msg) + assert_msg = "Object should be an IfcSlab" + assert snap_point.snap_object.split("/")[0] == "IfcSlab", assert_msg + _assert_pass(assert_msg) + + offset = 200 + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x + offset, y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x + offset, y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x + offset, y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x + offset, y) + yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x - offset, y) + snap_point = tool.Model.get_polyline_props().snap_mouse_point[0] + assert_msg = "Second click should have a snap_object" + assert snap_point.snap_object, assert_msg + _assert_pass(assert_msg) + assert_msg = "snap_object should be a string with the object name" + assert type(snap_point.snap_object) == str, assert_msg + _assert_pass(assert_msg) + assert_msg = "snap_type should be 'Face'" + assert snap_point.snap_type == "Face", assert_msg + _assert_pass(assert_msg) + assert_msg = "Object should be an IfcSlab" + assert snap_point.snap_object.split("/")[0] == "IfcSlab", assert_msg + _assert_pass(assert_msg) + + yield from preset_event_simulate(window, "RET", "TAP", x, y) + yield "FINISHED" + +def test_snap_in_xray_mode(window): + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", 0, 0) + area, region = get_area_and_region(window) + x = round(area.width * 0.68+ area.x) + y = round(area.height * 0.54 + area.y) -def test_snap_in_xray_mode(window, x, y): - area = next(area for area in bpy.context.screen.areas if area.type == "VIEW_3D") area.spaces[0].shading.show_xray = True yield from preset_event_simulate(window, "ESC", "TAP", x, y) @@ -167,13 +229,8 @@ def test_snap_in_xray_mode(window, x, y): measure_settings.measurement_type = "POLYLINE" for obj in tool.Blender.get_selected_objects(): obj.select_set(False) - for area in bpy.context.screen.areas: - if area.type == "VIEW_3D": - for region in area.regions: - if region.type == "WINDOW": - with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): - bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type="POLYLINE") - break + with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): + bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type="POLYLINE") yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x, y) @@ -187,29 +244,76 @@ def test_snap_in_xray_mode(window, x, y): assert_msg = "Object should be an IfcFurniture" assert snap_point.snap_object.split("/")[0] == "IfcFurniture", assert_msg _assert_pass(assert_msg) + + yield from preset_event_simulate(window, "RET", "TAP", x, y) yield "FINISHED" +def test_snap_far_from_origin(window): + bpy.context.view_layer.objects.active = None + bpy.ops.object.select_all(action="DESELECT") + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", 0, 0) + area, region = get_area_and_region(window) + x = round(area.width * 0.155 + area.x) + y = round(area.height * 0.18 + area.y) + + yield from preset_event_simulate(window, "ESC", "TAP", x, y) + + bpy.data.objects['IfcBuildingElementProxy/Cube'].select_set(True) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) + with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): + bpy.ops.view3d.view_selected() + + + measure_settings = tool.Project.get_measure_tool_settings() + measure_settings.measurement_type = "POLYLINE" + for obj in tool.Blender.get_selected_objects(): + obj.select_set(False) + with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): + bpy.ops.bim.measure_tool("INVOKE_DEFAULT", measure_type="POLYLINE") + + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) + yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) + yield from preset_event_simulate(window, "LEFTMOUSE", "TAP", x, y) + snap_point = tool.Model.get_polyline_props().snap_mouse_point[0] + assert_msg = "First click should have a snap_object" + assert snap_point.snap_object, assert_msg + _assert_pass(assert_msg) + assert_msg = "snap_object should be a string with the object name" + assert type(snap_point.snap_object) == str, assert_msg + _assert_pass(assert_msg) + assert_msg = "snap_type should be 'Vertex'" + assert snap_point.snap_type == "Vertex", assert_msg + _assert_pass(assert_msg) + print(snap_point.x) + print(round(snap_point.x, 3)) + assert_msg = "x should be 1000000" + assert round(snap_point.x, 3) == 1000.0, assert_msg + _assert_pass(assert_msg) + assert_msg = "y should be 1000000" + assert round(snap_point.y, 3) == 1000.0, assert_msg + _assert_pass(assert_msg) + + yield from preset_event_simulate(window, "RET", "TAP", x, y) + yield "FINISHED" def test_draw_polyline_wall(window, x, y): yield from preset_event_simulate(window, "ESC", "TAP", x, y) + area, region = get_area_and_region(window) for obj in tool.Blender.get_selected_objects(): obj.select_set(False) - for area in bpy.context.screen.areas: - if area.type == "VIEW_3D": - for region in area.regions: - if region.type == "WINDOW": - with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): - props = tool.Model.get_model_props() - ifc = tool.Ifc.get() - relating_type = ifc.by_type("IfcWallType")[0] + with bpy.context.temp_override(area=area, region=region, space_data=area.spaces[0]): + props = tool.Model.get_model_props() + ifc = tool.Ifc.get() + relating_type = ifc.by_type("IfcWallType")[0] - if tool.Model.get_usage_type(relating_type) == "LAYER2": - props.ifc_class = "IfcWallType" - props.relating_type_id = str(relating_type.id()) + if tool.Model.get_usage_type(relating_type) == "LAYER2": + props.ifc_class = "IfcWallType" + props.relating_type_id = str(relating_type.id()) - bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT") - break + bpy.ops.bim.draw_polyline_wall("INVOKE_DEFAULT") yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) yield from preset_event_simulate(window, "MOUSEMOVE", "NOTHING", x, y) @@ -251,8 +355,10 @@ def run_tests(): bpy.ops.bim.load_project(filepath=filepath) window = _get_valid_window() test_queue = [ - lambda w=window: test_snap_object_detection(w, 960, 540), - lambda w=window: test_snap_in_xray_mode(w, 1200, 540), + lambda w=window: test_snap_object_detection(w), + lambda w=window: test_snap_partially_behind_camera(w), + lambda w=window: test_snap_in_xray_mode(w), + lambda w=window: test_snap_far_from_origin(w), ] else: cleanup() @@ -266,7 +372,7 @@ def run_tests(): run_iter_from_timer( test_fn(), on_complete=_next, - on_error=lambda e: _handle_error(e, lambda: None), + on_error=lambda e: _handle_error(e, _next), ) _next()